[MM-23158] Group Mentions & Invites - Display group mentions in suggestions list in the main channel textbox (#4118)

* groups in group mention

* fix sorting issues

* address PR comments

* fix tests

* Update groups.ts

* fix nock

* fix translations

* Adding test

* fix linting

* update redux functions

* Add license check

* adddress PR comments

* remove lodash import

* fix lint problems

* revert package.json changes

* Address PR comments

* address PR comments

* fix naming

* address PR comments

* address PR comments

* Address comments found in second PR

* getAllGroups updated

* address PR comments

* Update app/mm-redux/utils/group_utils.ts

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

* revert change

* remove unneeded actions

* Use correct server version

* MM-26631: Fix order of group displaying

* MM-26633: Fix group mentions not showing up in group constrained team

* MM-26636: Group mentions not updating when role updates

* MM-26637: Group name not updating right away

* Address PR comments

* TRY AND catch

* address PR comments

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Hossein Ahmadian-Yazdi
2020-07-08 07:31:58 -04:00
committed by GitHub
parent 6c9dc8a917
commit b6eb3b9349
19 changed files with 536 additions and 976 deletions

View File

@@ -5,17 +5,19 @@ import {batchActions} from 'redux-batched-actions';
import {ViewTypes} from 'app/constants';
import {ChannelTypes, RoleTypes} from '@mm-redux/action_types';
import {ChannelTypes, RoleTypes, GroupTypes} from '@mm-redux/action_types';
import {
fetchMyChannelsAndMembers,
getChannelByNameAndTeamName,
leaveChannel as serviceLeaveChannel,
} from '@mm-redux/actions/channels';
import {savePreferences} from '@mm-redux/actions/preferences';
import {getLicense} from '@mm-redux/selectors/entities/general';
import {selectTeam} from '@mm-redux/actions/teams';
import {Client4} from '@mm-redux/client';
import {General, Preferences} from '@mm-redux/constants';
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {
getCurrentChannelId,
getRedirectChannelNameForTeam,
@@ -23,7 +25,7 @@ import {
isManuallyUnread,
} from '@mm-redux/selectors/entities/channels';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
import {getTeamByName} from '@mm-redux/selectors/entities/teams';
import {getTeamByName, getCurrentTeam} from '@mm-redux/selectors/entities/teams';
import {getChannelByName as selectChannelByName, getChannelsIdForTeam} from '@mm-redux/utils/channel_utils';
import EventEmitter from '@mm-redux/utils/event_emitter';
@@ -572,6 +574,71 @@ function setLoadMorePostsVisible(visible) {
};
}
function loadGroupData() {
return async (dispatch, getState) => {
const state = getState();
const actions = [];
const team = getCurrentTeam(state);
const serverVersion = state.entities.general.serverVersion;
const license = getLicense(state);
const hasLicense = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
if (hasLicense && team && isMinimumServerVersion(serverVersion, 5, 24)) {
for (let i = 0; i <= MAX_RETRIES; i++) {
try {
if (team.group_constrained) {
const [getAllGroupsAssociatedToChannelsInTeam, getAllGroupsAssociatedToTeam] = await Promise.all([ //eslint-disable-line no-await-in-loop
Client4.getAllGroupsAssociatedToChannelsInTeam(team.id, true),
Client4.getAllGroupsAssociatedToTeam(team.id, true),
]);
if (getAllGroupsAssociatedToChannelsInTeam.groups) {
actions.push({
type: GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNELS_IN_TEAM,
data: {groupsByChannelId: getAllGroupsAssociatedToChannelsInTeam.groups},
});
}
if (getAllGroupsAssociatedToTeam) {
actions.push({
type: GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_TEAM,
data: {...getAllGroupsAssociatedToTeam, teamID: team.id},
});
}
} else {
const [getAllGroupsAssociatedToChannelsInTeam, getGroups] = await Promise.all([ //eslint-disable-line no-await-in-loop
Client4.getAllGroupsAssociatedToChannelsInTeam(team.id, true),
Client4.getGroups(true),
]);
if (getAllGroupsAssociatedToChannelsInTeam.groups) {
actions.push({
type: GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNELS_IN_TEAM,
data: {groupsByChannelId: getAllGroupsAssociatedToChannelsInTeam.groups},
});
}
if (getGroups) {
actions.push({
type: GroupTypes.RECEIVED_GROUPS,
data: getGroups,
});
}
}
} catch (err) {
return {error: err};
}
}
}
if (actions.length) {
dispatch(batchActions(actions, 'BATCH_GROUP_DATA'));
}
return {data: true};
};
}
export function loadChannelsForTeam(teamId, skipDispatch = false) {
return async (dispatch, getState) => {
const state = getState();
@@ -642,6 +709,8 @@ export function loadChannelsForTeam(teamId, skipDispatch = false) {
dispatch(loadUnreadChannelPosts(data.channels, data.channelMembers));
}
dispatch(loadGroupData());
}
return {data};

View File

@@ -3,8 +3,7 @@
import {Client4} from '@mm-redux/client';
import websocketClient from '@websocket';
import {ChannelTypes, GeneralTypes, EmojiTypes, PostTypes, PreferenceTypes, TeamTypes, UserTypes, RoleTypes, IntegrationTypes} from '@mm-redux/action_types';
import {ChannelTypes, GeneralTypes, EmojiTypes, PostTypes, PreferenceTypes, TeamTypes, UserTypes, RoleTypes, IntegrationTypes, GroupTypes} from '@mm-redux/action_types';
import {General, Preferences} from '@mm-redux/constants';
import {
getAllChannels,
@@ -298,6 +297,8 @@ function handleEvent(msg: WebSocketMessage) {
return dispatch(handleRoleUpdatedEvent(msg));
case WebsocketEvents.USER_ROLE_UPDATED:
return dispatch(handleUserRoleUpdated(msg));
case WebsocketEvents.MEMBERROLE_UPDATED:
return dispatch(handleUpdateMemberRoleEvent(msg));
case WebsocketEvents.CHANNEL_CREATED:
return dispatch(handleChannelCreatedEvent(msg));
case WebsocketEvents.CHANNEL_DELETED:
@@ -341,6 +342,8 @@ function handleEvent(msg: WebSocketMessage) {
return dispatch(handleConfigChangedEvent(msg));
case WebsocketEvents.OPEN_DIALOG:
return dispatch(handleOpenDialogEvent(msg));
case WebsocketEvents.RECEIVED_GROUP:
return dispatch(handleGroupUpdatedEvent(msg));
}
return {data: true};
@@ -958,6 +961,34 @@ function handleChannelSchemeUpdatedEvent(msg: WebSocketMessage) {
};
}
function handleUpdateMemberRoleEvent(msg: WebSocketMessage) {
return async (dispatch: DispatchFunc) => {
const memberData = JSON.parse(msg.data.member);
const roles = memberData.roles.split(' ');
const actions = [];
try {
const newRoles = await Client4.getRolesByNames(roles);
if (newRoles.length) {
actions.push({
type: RoleTypes.RECEIVED_ROLES,
data: newRoles,
});
}
} catch (error) {
return {error};
}
actions.push({
type: TeamTypes.RECEIVED_MY_TEAM_MEMBER,
data: memberData,
});
dispatch(batchActions(actions));
return {data: true};
};
}
function handleDirectAddedEvent(msg: WebSocketMessage) {
return async (dispatch: DispatchFunc) => {
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
@@ -1142,6 +1173,23 @@ function handleOpenDialogEvent(msg: WebSocketMessage) {
};
}
function handleGroupUpdatedEvent(msg: WebSocketMessage) {
return (dispatch: DispatchFunc) => {
const data = JSON.parse(msg.data.group);
dispatch(batchActions([
{
type: GroupTypes.RECEIVED_GROUP,
data,
},
{
type: GroupTypes.RECEIVED_MY_GROUPS,
data: [data],
},
]));
return {data: true};
};
}
// Helpers
export async function notVisibleUsersActions(state: GlobalState): Promise<Array<GenericAction>> {
if (!isMinimumServerVersion(Client4.getServerVersion(), 5, 23)) {

View File

@@ -12,6 +12,7 @@ import AtMentionItem from 'app/components/autocomplete/at_mention_item';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
import GroupMentionItem from 'app/components/autocomplete/at_mention_group/at_mention_group';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
@@ -38,6 +39,7 @@ export default class AtMention extends PureComponent {
isLandscape: PropTypes.bool.isRequired,
nestedScrollEnabled: PropTypes.bool,
useChannelMentions: PropTypes.bool.isRequired,
groups: PropTypes.array,
};
static defaultProps = {
@@ -106,7 +108,7 @@ export default class AtMention extends PureComponent {
}
buildSections = (props) => {
const {isSearch, inChannel, outChannel, teamMembers, matchTerm} = props;
const {isSearch, inChannel, outChannel, teamMembers, matchTerm, groups} = props;
const sections = [];
if (isSearch) {
@@ -126,6 +128,16 @@ export default class AtMention extends PureComponent {
});
}
if (groups.length) {
sections.push({
id: t('suggestion.mention.groups'),
defaultMessage: 'Group Mentions',
data: groups,
key: 'groups',
renderItem: this.renderGroupMentions,
});
}
if (this.props.useChannelMentions && this.checkSpecialMentions(matchTerm)) {
sections.push({
id: t('suggestion.mention.special'),
@@ -186,7 +198,6 @@ export default class AtMention extends PureComponent {
} else {
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
}
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
}
@@ -228,10 +239,20 @@ export default class AtMention extends PureComponent {
);
};
renderGroupMentions = ({item}) => {
return (
<GroupMentionItem
key={`autocomplete-group-${item.name}`}
completeHandle={item.name}
onPress={this.completeMention}
theme={this.props.theme}
/>
);
};
render() {
const {maxListHeight, theme, nestedScrollEnabled} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
// If we are not in an active state or the mention has been completed return null so nothing is rendered
// other components are not blocked.

View File

@@ -6,7 +6,9 @@ import {connect} from 'react-redux';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {autocompleteUsers} from '@mm-redux/actions/users';
import {getLicense} from '@mm-redux/selectors/entities/general';
import {getCurrentChannelId, getDefaultChannel} from '@mm-redux/selectors/entities/channels';
import {getAssociatedGroupsForReference, searchAssociatedGroupsForReferenceLocal} from '@mm-redux/selectors/entities/groups';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {isLandscape} from 'app/selectors/device';
@@ -26,7 +28,9 @@ import AtMention from './at_mention';
function mapStateToProps(state, ownProps) {
const {cursorPosition, isSearch} = ownProps;
const currentChannelId = getCurrentChannelId(state);
const currentTeamId = getCurrentTeamId(state);
const license = getLicense(state);
const hasLicense = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
let useChannelMentions = true;
if (isMinimumServerVersion(state.entities.general.serverVersion, 5, 22)) {
useChannelMentions = haveIChannelPermission(
@@ -45,6 +49,7 @@ function mapStateToProps(state, ownProps) {
let teamMembers;
let inChannel;
let outChannel;
let groups = [];
if (isSearch) {
teamMembers = filterMembersInCurrentTeam(state, matchTerm);
} else {
@@ -52,9 +57,17 @@ function mapStateToProps(state, ownProps) {
outChannel = filterMembersNotInChannel(state, matchTerm);
}
if (hasLicense && isMinimumServerVersion(state.entities.general.serverVersion, 5, 24)) {
if (matchTerm) {
groups = searchAssociatedGroupsForReferenceLocal(state, matchTerm, currentTeamId, currentChannelId);
} else {
groups = getAssociatedGroupsForReference(state, currentTeamId, currentChannelId);
}
}
return {
currentChannelId,
currentTeamId: getCurrentTeamId(state),
currentTeamId,
defaultChannel: getDefaultChannel(state),
matchTerm,
teamMembers,
@@ -64,6 +77,7 @@ function mapStateToProps(state, ownProps) {
theme: getTheme(state),
isLandscape: isLandscape(state),
useChannelMentions,
groups,
};
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
View,
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class GroupMentionItem extends PureComponent {
static propTypes = {
completeHandle: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
completeMention = () => {
const {onPress, completeHandle} = this.props;
onPress(completeHandle);
};
render() {
const {
completeHandle,
theme,
} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableWithFeedback
onPress={this.completeMention}
style={style.row}
type={'opacity'}
>
<View style={style.rowPicture}>
<Icon
name='users'
style={style.rowIcon}
/>
</View>
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<Text style={style.rowFullname}>{`${completeHandle}`}</Text>
</TouchableWithFeedback>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
},
rowPicture: {
marginHorizontal: 8,
width: 20,
alignItems: 'center',
justifyContent: 'center',
},
rowIcon: {
color: changeOpacity(theme.centerChannelColor, 0.7),
fontSize: 14,
},
rowUsername: {
fontSize: 13,
color: theme.centerChannelColor,
},
rowFullname: {
color: theme.centerChannelColor,
flex: 1,
opacity: 0.6,
},
textWrapper: {
flex: 1,
flexWrap: 'wrap',
paddingRight: 8,
},
};
});

View File

@@ -41,5 +41,7 @@ const WebsocketEvents = {
PLUGIN_STATUSES_CHANGED: 'plugin_statuses_changed',
OPEN_DIALOG: 'open_dialog',
INCREASE_POST_VISIBILITY_BY_ONE: 'increase_post_visibility_by_one',
MEMBERROLE_UPDATED: 'memberrole_updated',
RECEIVED_GROUP: 'received_group',
};
export default WebsocketEvents;

View File

@@ -11,9 +11,14 @@ export default keyMirror({
RECEIVED_GROUP_TEAMS: null,
RECEIVED_GROUP_CHANNELS: null,
RECEIVED_MY_GROUPS: null,
RECEIVED_GROUP_MEMBERS: null,
RECEIVED_GROUP_ASSOCIATED_TO_TEAM: null,
RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNELS_IN_TEAM: null,
RECEIVED_GROUP: null,
RECEIVED_GROUPS: null,

View File

@@ -1,693 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import assert from 'assert';
import nock from 'nock';
import * as Actions from '@mm-redux/actions/groups';
import {Client4} from '@mm-redux/client';
import {Groups} from '../constants';
import TestHelper from 'test/test_helper';
import configureStore from 'test/test_store';
describe('Actions.Groups', () => {
let store;
beforeEach(async () => {
await TestHelper.initBasic(Client4);
store = await configureStore();
});
afterEach(async () => {
await TestHelper.tearDown();
});
it('getGroupSyncables', async () => {
const groupID = '5rgoajywb3nfbdtyafbod47rya';
const groupTeams = [
{
team_id: 'ge63nq31sbfy3duzq5f7yqn1kh',
team_display_name: 'dolphins',
team_type: 'O',
group_id: '5rgoajywb3nfbdtyafbod47rya',
auto_add: true,
create_at: 1542643748412,
delete_at: 0,
update_at: 1542643748412,
},
{
team_id: 'tdjrcr3hg7yazyos17a53jduna',
team_display_name: 'developers',
team_type: 'O',
group_id: '5rgoajywb3nfbdtyafbod47rya',
auto_add: true,
create_at: 1542643825026,
delete_at: 0,
update_at: 1542643825026,
},
];
const groupChannels = [
{
channel_id: 'o3tdawqxot8kikzq8bk54zggbc',
channel_display_name: 'standup',
channel_type: 'P',
team_id: 'tdjrcr3hg7yazyos17a53jduna',
team_display_name: 'developers',
team_type: 'O',
group_id: '5rgoajywb3nfbdtyafbod47rya',
auto_add: true,
create_at: 1542644105041,
delete_at: 0,
update_at: 1542644105041,
},
{
channel_id: 's6oxu3embpdepyprx1fn5gjhea',
channel_display_name: 'swimming',
channel_type: 'P',
team_id: 'ge63nq31sbfy3duzq5f7yqn1kh',
team_display_name: 'dolphins',
team_type: 'O',
group_id: '5rgoajywb3nfbdtyafbod47rya',
auto_add: true,
create_at: 1542644105042,
delete_at: 0,
update_at: 1542644105042,
},
];
nock(Client4.getBaseRoute()).
get(`/groups/${groupID}/teams`).
reply(200, groupTeams);
nock(Client4.getBaseRoute()).
get(`/groups/${groupID}/channels`).
reply(200, groupChannels);
await Actions.getGroupSyncables(groupID, Groups.SYNCABLE_TYPE_TEAM)(store.dispatch, store.getState);
await Actions.getGroupSyncables(groupID, Groups.SYNCABLE_TYPE_CHANNEL)(store.dispatch, store.getState);
const state = store.getState();
const groupSyncables = state.entities.groups.syncables[groupID];
assert.ok(groupSyncables);
for (let i = 0; i < 2; i++) {
assert.ok(JSON.stringify(groupSyncables.teams[i]) === JSON.stringify(groupTeams[i]));
assert.ok(JSON.stringify(groupSyncables.channels[i]) === JSON.stringify(groupChannels[i]));
}
});
it('getGroupMembers', async () => {
const groupID = '5rgoajywb3nfbdtyafbod47rya';
const response = {
members: [
{
id: 'ok1mtgwrn7gbzetzfdgircykir',
create_at: 1542658437708,
update_at: 1542658441412,
delete_at: 0,
username: 'test.161927',
auth_data: 'test.161927',
auth_service: 'ldap',
email: 'success+test.161927@simulator.amazonses.com',
email_verified: true,
nickname: '',
first_name: 'test',
last_name: 'test.161927',
position: '',
roles: 'system_user',
notify_props: {
channel: 'true',
comments: 'never',
desktop: 'mention',
desktop_sound: 'true',
email: 'true',
first_name: 'false',
mention_keys: 'test.161927,@test.161927',
push: 'mention',
push_status: 'away',
},
last_password_update: 1542658437708,
locale: 'en',
timezone: {
automaticTimezone: '',
manualTimezone: '',
useAutomaticTimezone: 'true',
},
},
],
total_member_count: 1,
};
nock(Client4.getBaseRoute()).
get(`/groups/${groupID}/members?page=0&per_page=100`).
reply(200, response);
await Actions.getGroupMembers(groupID, 0, 100)(store.dispatch, store.getState);
const state = store.getState();
const groupMembers = state.entities.groups.members;
assert.ok(groupMembers);
assert.ok(groupMembers[groupID].totalMemberCount === response.total_member_count);
assert.ok(JSON.stringify(response.members[0]) === JSON.stringify(groupMembers[groupID].members[0]));
});
it('getGroup', async () => {
const groupID = '5rgoajywb3nfbdtyafbod47rya';
const response = {
id: '5rgoajywb3nfbdtyafbod47rya',
name: '8b7ks7ngqbgndqutka48gfzaqh',
display_name: 'Test Group 0',
description: '',
type: 'ldap',
remote_id: '\\eb\\80\\94\\cd\\d4\\32\\7c\\45\\87\\79\\1b\\fe\\45\\d9\\ac\\7b',
create_at: 1542399032816,
update_at: 1542399032816,
delete_at: 0,
has_syncables: false,
};
nock(Client4.getBaseRoute()).
get(`/groups/${groupID}`).
reply(200, response);
await Actions.getGroup(groupID)(store.dispatch, store.getState);
const state = store.getState();
const groups = state.entities.groups.groups;
assert.ok(groups);
assert.ok(groups[groupID]);
assert.ok(JSON.stringify(response) === JSON.stringify(groups[groupID]));
});
it('linkGroupSyncable', async () => {
const groupID = '5rgoajywb3nfbdtyafbod47rya';
const teamID = 'ge63nq31sbfy3duzq5f7yqn1kh';
const channelID = 'o3tdawqxot8kikzq8bk54zggbc';
const groupTeamResponse = {
team_id: 'ge63nq31sbfy3duzq5f7yqn1kh',
group_id: '5rgoajywb3nfbdtyafbod47rya',
auto_add: true,
create_at: 1542643748412,
delete_at: 0,
update_at: 1542660566032,
};
const groupChannelResponse = {
channel_id: 'o3tdawqxot8kikzq8bk54zggbc',
group_id: '5rgoajywb3nfbdtyafbod47rya',
auto_add: true,
create_at: 1542644105041,
delete_at: 0,
update_at: 1542662607342,
};
nock(Client4.getBaseRoute()).
post(`/groups/${groupID}/teams/${teamID}/link`).
reply(200, groupTeamResponse);
nock(Client4.getBaseRoute()).
post(`/groups/${groupID}/channels/${channelID}/link`).
reply(200, groupChannelResponse);
await Actions.linkGroupSyncable(groupID, teamID, Groups.SYNCABLE_TYPE_TEAM)(store.dispatch, store.getState);
await Actions.linkGroupSyncable(groupID, channelID, Groups.SYNCABLE_TYPE_CHANNEL)(store.dispatch, store.getState);
const state = store.getState();
const syncables = state.entities.groups.syncables;
assert.ok(syncables[groupID]);
assert.ok(JSON.stringify(syncables[groupID].teams[0]) === JSON.stringify(groupTeamResponse));
assert.ok(JSON.stringify(syncables[groupID].channels[0]) === JSON.stringify(groupChannelResponse));
});
it('unlinkGroupSyncable', async () => {
const groupID = '5rgoajywb3nfbdtyafbod47rya';
const teamID = 'ge63nq31sbfy3duzq5f7yqn1kh';
const channelID = 'o3tdawqxot8kikzq8bk54zggbc';
const groupTeamResponse = {
team_id: 'ge63nq31sbfy3duzq5f7yqn1kh',
group_id: '5rgoajywb3nfbdtyafbod47rya',
auto_add: true,
create_at: 1542643748412,
delete_at: 0,
update_at: 1542660566032,
};
const groupChannelResponse = {
channel_id: 'o3tdawqxot8kikzq8bk54zggbc',
group_id: '5rgoajywb3nfbdtyafbod47rya',
auto_add: true,
create_at: 1542644105041,
delete_at: 0,
update_at: 1542662607342,
};
nock(Client4.getBaseRoute()).
post(`/groups/${groupID}/teams/${teamID}/link`).
reply(200, groupTeamResponse);
nock(Client4.getBaseRoute()).
post(`/groups/${groupID}/channels/${channelID}/link`).
reply(200, groupChannelResponse);
await Actions.linkGroupSyncable(groupID, teamID, Groups.SYNCABLE_TYPE_TEAM)(store.dispatch, store.getState);
await Actions.linkGroupSyncable(groupID, channelID, Groups.SYNCABLE_TYPE_CHANNEL)(store.dispatch, store.getState);
let state = store.getState();
let syncables = state.entities.groups.syncables;
assert.ok(syncables[groupID]);
assert.ok(JSON.stringify(syncables[groupID].teams[0]) === JSON.stringify(groupTeamResponse));
assert.ok(JSON.stringify(syncables[groupID].channels[0]) === JSON.stringify(groupChannelResponse));
const beforeTeamsLength = syncables[groupID].teams.length;
const beforeChannelsLength = syncables[groupID].channels.length;
nock(Client4.getBaseRoute()).
delete(`/groups/${groupID}/teams/${teamID}/link`).
reply(204, {ok: true});
nock(Client4.getBaseRoute()).
delete(`/groups/${groupID}/channels/${channelID}/link`).
reply(204, {ok: true});
await Actions.unlinkGroupSyncable(groupID, teamID, Groups.SYNCABLE_TYPE_TEAM)(store.dispatch, store.getState);
await Actions.unlinkGroupSyncable(groupID, channelID, Groups.SYNCABLE_TYPE_CHANNEL)(store.dispatch, store.getState);
state = store.getState();
syncables = state.entities.groups.syncables;
assert.ok(syncables[groupID]);
assert.ok(syncables[groupID].teams.length === beforeTeamsLength - 1);
assert.ok(syncables[groupID].channels.length === beforeChannelsLength - 1);
});
it('getAllGroupsAssociatedToTeam', async () => {
const teamID = '5rgoajywb3nfbdtyafbod47ryb';
const response = {
groups: [
{
id: 'xh585kyz3tn55q6ipfo57btwnc',
name: '9uobsi3xb3y5tfjb3ze7umnh1o',
display_name: 'abc',
description: '',
source: 'ldap',
remote_id: 'abc',
create_at: 1553808969975,
update_at: 1553808969975,
delete_at: 0,
has_syncables: false,
member_count: 2,
},
{
id: 'tnd8zod9f3fdtqosxjmhwucbth',
name: 'nobctj4brfgtpj3a1peiyq47tc',
display_name: 'engineering',
description: '',
source: 'ldap',
remote_id: 'engineering',
create_at: 1553808971099,
update_at: 1553808971099,
delete_at: 0,
has_syncables: false,
member_count: 8,
},
{
id: 'qhdp6g7aubbpiyja7c4sgpe7tc',
name: 'x5bjwa4kwirpmqudhp5dterine',
display_name: 'qa',
description: '',
source: 'ldap',
remote_id: 'qa',
create_at: 1553808971548,
update_at: 1553808971548,
delete_at: 0,
has_syncables: false,
member_count: 2,
},
],
total_group_count: 3,
};
nock(Client4.getBaseRoute()).
get(`/teams/${teamID}/groups?paginate=false`).
reply(200, response);
await Actions.getAllGroupsAssociatedToTeam(teamID)(store.dispatch, store.getState);
const state = store.getState();
const groupIDs = state.entities.teams.groupsAssociatedToTeam[teamID].ids;
assert.strictEqual(groupIDs.length, response.groups.length);
groupIDs.forEach((id) => {
assert.ok(response.groups.map((group) => group.id).includes(id));
});
});
it('getGroupsAssociatedToTeam', async () => {
const teamID = '5rgoajywb3nfbdtyafbod47ryb';
store = await configureStore({
entities: {
teams: {
groupsAssociatedToTeam: {
[teamID]: ['tnd8zod9f3fdtqosxjmhwucbth', 'qhdp6g7aubbpiyja7c4sgpe7tc'],
},
},
},
});
const response = {
groups: [
{
id: 'tnd8zod9f3fdtqosxjmhwucbth',
name: 'nobctj4brfgtpj3a1peiyq47tc',
display_name: 'engineering',
description: '',
source: 'ldap',
remote_id: 'engineering',
create_at: 1553808971099,
update_at: 1553808971099,
delete_at: 0,
has_syncables: false,
member_count: 8,
},
{
id: 'qhdp6g7aubbpiyja7c4sgpe7tc',
name: 'x5bjwa4kwirpmqudhp5dterine',
display_name: 'qa',
description: '',
source: 'ldap',
remote_id: 'qa',
create_at: 1553808971548,
update_at: 1553808971548,
delete_at: 0,
has_syncables: false,
member_count: 2,
},
],
total_group_count: 3,
};
nock(Client4.getBaseRoute()).
get(`/teams/${teamID}/groups?page=100&per_page=60&q=0&include_member_count=true`).
reply(200, response);
await Actions.getGroupsAssociatedToTeam(teamID, 0, 100)(store.dispatch, store.getState);
const state = store.getState();
const groupIDs = state.entities.teams.groupsAssociatedToTeam[teamID].ids;
const expectedIDs = ['tnd8zod9f3fdtqosxjmhwucbth', 'qhdp6g7aubbpiyja7c4sgpe7tc'];
assert.strictEqual(groupIDs.length, expectedIDs.length);
groupIDs.forEach((id) => {
assert.ok(expectedIDs.includes(id));
assert.ok(state.entities.groups.groups[id]);
});
const count = state.entities.teams.groupsAssociatedToTeam[teamID].totalCount;
assert.equal(count, response.total_group_count);
});
it('getGroupsNotAssociatedToTeam', async () => {
const teamID = '5rgoajywb3nfbdtyafbod47ryb';
store = await configureStore({
entities: {
teams: {
groupsAssociatedToTeam: {
[teamID]: {ids: ['existing1', 'existing2']},
},
},
},
});
const response = [
{
id: 'existing1',
name: 'nobctj4brfgtpj3a1peiyq47tc',
display_name: 'engineering',
description: '',
source: 'ldap',
remote_id: 'engineering',
create_at: 1553808971099,
update_at: 1553808971099,
delete_at: 0,
has_syncables: false,
member_count: 8,
},
];
nock(Client4.getBaseRoute()).
get(`/groups?not_associated_to_team=${teamID}&page=100&per_page=60&q=0&include_member_count=true`).
reply(200, response);
await Actions.getGroupsNotAssociatedToTeam(teamID, 0, 100)(store.dispatch, store.getState);
const state = store.getState();
const groupIDs = state.entities.teams.groupsAssociatedToTeam[teamID].ids;
const expectedIDs = ['existing2'].concat(response.map((group) => group.id));
assert.strictEqual(groupIDs.length, expectedIDs.length);
groupIDs.forEach((id) => {
assert.ok(expectedIDs.includes(id));
});
});
it('getAllGroupsAssociatedToChannel', async () => {
const channelID = '5rgoajywb3nfbdtyafbod47ryb';
const response = {
groups: [
{
id: 'xh585kyz3tn55q6ipfo57btwnc',
name: '9uobsi3xb3y5tfjb3ze7umnh1o',
display_name: 'abc',
description: '',
source: 'ldap',
remote_id: 'abc',
create_at: 1553808969975,
update_at: 1553808969975,
delete_at: 0,
has_syncables: false,
member_count: 2,
},
{
id: 'tnd8zod9f3fdtqosxjmhwucbth',
name: 'nobctj4brfgtpj3a1peiyq47tc',
display_name: 'engineering',
description: '',
source: 'ldap',
remote_id: 'engineering',
create_at: 1553808971099,
update_at: 1553808971099,
delete_at: 0,
has_syncables: false,
member_count: 8,
},
{
id: 'qhdp6g7aubbpiyja7c4sgpe7tc',
name: 'x5bjwa4kwirpmqudhp5dterine',
display_name: 'qa',
description: '',
source: 'ldap',
remote_id: 'qa',
create_at: 1553808971548,
update_at: 1553808971548,
delete_at: 0,
has_syncables: false,
member_count: 2,
},
],
total_group_count: 3,
};
nock(Client4.getBaseRoute()).
get(`/channels/${channelID}/groups?paginate=false`).
reply(200, response);
await Actions.getAllGroupsAssociatedToChannel(channelID)(store.dispatch, store.getState);
const state = store.getState();
const groupIDs = state.entities.channels.groupsAssociatedToChannel[channelID].ids;
assert.strictEqual(groupIDs.length, response.groups.length);
groupIDs.forEach((id) => {
assert.ok(response.groups.map((group) => group.id).includes(id));
});
});
it('getGroupsAssociatedToChannel', async () => {
const channelID = '5rgoajywb3nfbdtyafbod47ryb';
store = await configureStore({
entities: {
channels: {
groupsAssociatedToChannel: {
[channelID]: ['tnd8zod9f3fdtqosxjmhwucbth', 'qhdp6g7aubbpiyja7c4sgpe7tc'],
},
},
},
});
const response = {
groups: [
{
id: 'tnd8zod9f3fdtqosxjmhwucbth',
name: 'nobctj4brfgtpj3a1peiyq47tc',
display_name: 'engineering',
description: '',
source: 'ldap',
remote_id: 'engineering',
create_at: 1553808971099,
update_at: 1553808971099,
delete_at: 0,
has_syncables: false,
member_count: 8,
},
{
id: 'qhdp6g7aubbpiyja7c4sgpe7tc',
name: 'x5bjwa4kwirpmqudhp5dterine',
display_name: 'qa',
description: '',
source: 'ldap',
remote_id: 'qa',
create_at: 1553808971548,
update_at: 1553808971548,
delete_at: 0,
has_syncables: false,
member_count: 2,
},
],
total_group_count: 3,
};
nock(Client4.getBaseRoute()).
get(`/channels/${channelID}/groups?page=100&per_page=60&q=0&include_member_count=true`).
reply(200, response);
await Actions.getGroupsAssociatedToChannel(channelID, 0, 100)(store.dispatch, store.getState);
const state = store.getState();
const groupIDs = state.entities.channels.groupsAssociatedToChannel[channelID].ids;
const expectedIDs = ['tnd8zod9f3fdtqosxjmhwucbth', 'qhdp6g7aubbpiyja7c4sgpe7tc'];
assert.strictEqual(groupIDs.length, expectedIDs.length);
groupIDs.forEach((id) => {
assert.ok(expectedIDs.includes(id));
assert.ok(state.entities.groups.groups[id]);
});
const count = state.entities.channels.groupsAssociatedToChannel[channelID].totalCount;
assert.equal(count, response.total_group_count);
});
it('getGroupsNotAssociatedToChannel', async () => {
const channelID = '5rgoajywb3nfbdtyafbod47ryb';
store = await configureStore({
entities: {
channels: {
groupsAssociatedToChannel: {
[channelID]: {ids: ['existing1', 'existing2']},
},
},
},
});
const response = [
{
id: 'existing1',
name: 'nobctj4brfgtpj3a1peiyq47tc',
display_name: 'engineering',
description: '',
source: 'ldap',
remote_id: 'engineering',
create_at: 1553808971099,
update_at: 1553808971099,
delete_at: 0,
has_syncables: false,
member_count: 8,
},
];
nock(Client4.getBaseRoute()).
get(`/groups?not_associated_to_channel=${channelID}&page=100&per_page=60&q=0&include_member_count=true`).
reply(200, response);
await Actions.getGroupsNotAssociatedToChannel(channelID, 0, 100)(store.dispatch, store.getState);
const state = store.getState();
const groupIDs = state.entities.channels.groupsAssociatedToChannel[channelID].ids;
const expectedIDs = ['existing2'].concat(response.map((group) => group.id));
assert.strictEqual(groupIDs.length, expectedIDs.length);
groupIDs.forEach((id) => {
assert.ok(expectedIDs.includes(id));
});
});
it('patchGroupSyncable', async () => {
const groupID = '5rgoajywb3nfbdtyafbod47rya';
const teamID = 'ge63nq31sbfy3duzq5f7yqn1kh';
const channelID = 'o3tdawqxot8kikzq8bk54zggbc';
const groupSyncablePatch = {
auto_add: true,
scheme_admin: true,
};
const groupTeamResponse = {
team_id: 'ge63nq31sbfy3duzq5f7yqn1kh',
group_id: '5rgoajywb3nfbdtyafbod47rya',
auto_add: true,
scheme_admin: true,
create_at: 1542643748412,
delete_at: 0,
update_at: 1542660566032,
};
const groupChannelResponse = {
channel_id: 'o3tdawqxot8kikzq8bk54zggbc',
group_id: '5rgoajywb3nfbdtyafbod47rya',
auto_add: true,
scheme_admin: true,
create_at: 1542644105041,
delete_at: 0,
update_at: 1542662607342,
};
nock(Client4.getBaseRoute()).
put(`/groups/${groupID}/teams/${teamID}/patch`).
reply(200, groupTeamResponse);
nock(Client4.getBaseRoute()).
put(`/groups/${groupID}/channels/${channelID}/patch`).
reply(200, groupChannelResponse);
await Actions.patchGroupSyncable(groupID, teamID, Groups.SYNCABLE_TYPE_TEAM, groupSyncablePatch)(store.dispatch, store.getState);
await Actions.patchGroupSyncable(groupID, channelID, Groups.SYNCABLE_TYPE_CHANNEL, groupSyncablePatch)(store.dispatch, store.getState);
const state = store.getState();
const groupSyncables = state.entities.groups.syncables[groupID];
assert.ok(groupSyncables);
assert.ok(groupSyncables.teams[0].auto_add === groupSyncablePatch.auto_add);
assert.ok(groupSyncables.channels[0].auto_add === groupSyncablePatch.auto_add);
assert.ok(groupSyncables.teams[0].scheme_admin === groupSyncablePatch.scheme_admin);
assert.ok(groupSyncables.channels[0].scheme_admin === groupSyncablePatch.scheme_admin);
});
});

View File

@@ -1,258 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {GroupTypes} from '@mm-redux/action_types';
import {General, Groups} from '../constants';
import {Client4} from '@mm-redux/client';
import {Action, ActionFunc, batchActions, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
import {SyncableType, SyncablePatch} from '@mm-redux/types/groups';
import {logError} from './errors';
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
export function linkGroupSyncable(groupID: string, syncableID: string, syncableType: SyncableType, patch: SyncablePatch): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data;
try {
data = await Client4.linkGroupSyncable(groupID, syncableID, syncableType, patch);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
const dispatches: Action[] = [];
let type = '';
switch (syncableType) {
case Groups.SYNCABLE_TYPE_TEAM:
dispatches.push({type: GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_TEAM, data: {teamID: syncableID, groups: [{id: groupID}]}});
type = GroupTypes.LINKED_GROUP_TEAM;
break;
case Groups.SYNCABLE_TYPE_CHANNEL:
type = GroupTypes.LINKED_GROUP_CHANNEL;
break;
default:
console.warn(`unhandled syncable type ${syncableType}`); // eslint-disable-line no-console
}
dispatches.push({type, data});
dispatch(batchActions(dispatches));
return {data: true};
};
}
export function unlinkGroupSyncable(groupID: string, syncableID: string, syncableType: SyncableType): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
try {
await Client4.unlinkGroupSyncable(groupID, syncableID, syncableType);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
const dispatches: Action[] = [];
let type = '';
const data = {group_id: groupID, syncable_id: syncableID};
switch (syncableType) {
case Groups.SYNCABLE_TYPE_TEAM:
type = GroupTypes.UNLINKED_GROUP_TEAM;
data.syncable_id = syncableID;
dispatches.push({type: GroupTypes.RECEIVED_GROUPS_NOT_ASSOCIATED_TO_TEAM, data: {teamID: syncableID, groups: [{id: groupID}]}});
break;
case Groups.SYNCABLE_TYPE_CHANNEL:
type = GroupTypes.UNLINKED_GROUP_CHANNEL;
data.syncable_id = syncableID;
break;
default:
console.warn(`unhandled syncable type ${syncableType}`); // eslint-disable-line no-console
}
dispatches.push({type, data});
dispatch(batchActions(dispatches));
return {data: true};
};
}
export function getGroupSyncables(groupID: string, syncableType: SyncableType): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data;
try {
data = await Client4.getGroupSyncables(groupID, syncableType);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
let type = '';
switch (syncableType) {
case Groups.SYNCABLE_TYPE_TEAM:
type = GroupTypes.RECEIVED_GROUP_TEAMS;
break;
case Groups.SYNCABLE_TYPE_CHANNEL:
type = GroupTypes.RECEIVED_GROUP_CHANNELS;
break;
default:
console.warn(`unhandled syncable type ${syncableType}`); // eslint-disable-line no-console
}
dispatch(batchActions([
{type, data, group_id: groupID},
]));
return {data: true};
};
}
export function patchGroupSyncable(groupID: string, syncableID: string, syncableType: SyncableType, patch: SyncablePatch): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data;
try {
data = await Client4.patchGroupSyncable(groupID, syncableID, syncableType, patch);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
return {error};
}
const dispatches: Action[] = [];
let type = '';
switch (syncableType) {
case Groups.SYNCABLE_TYPE_TEAM:
type = GroupTypes.PATCHED_GROUP_TEAM;
break;
case Groups.SYNCABLE_TYPE_CHANNEL:
type = GroupTypes.PATCHED_GROUP_CHANNEL;
break;
default:
console.warn(`unhandled syncable type ${syncableType}`); // eslint-disable-line no-console
}
dispatches.push(
{type, data},
);
dispatch(batchActions(dispatches));
return {data: true};
};
}
export function getGroupMembers(groupID: string, page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
let data;
try {
data = await Client4.getGroupMembers(groupID, page, perPage);
} catch (error) {
forceLogoutIfNecessary(error, dispatch, getState);
dispatch(logError(error));
return {error};
}
dispatch(batchActions([
{type: GroupTypes.RECEIVED_GROUP_MEMBERS, group_id: groupID, data},
]));
return {data: true};
};
}
export function getGroup(id: string): ActionFunc {
return bindClientFunc({
clientFunc: Client4.getGroup,
onSuccess: [GroupTypes.RECEIVED_GROUP],
params: [
id,
],
});
}
export function getGroupsNotAssociatedToTeam(teamID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc {
return bindClientFunc({
clientFunc: Client4.getGroupsNotAssociatedToTeam,
onSuccess: [GroupTypes.RECEIVED_GROUPS],
params: [
teamID,
q,
page,
perPage,
],
});
}
export function getGroupsNotAssociatedToChannel(channelID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc {
return bindClientFunc({
clientFunc: Client4.getGroupsNotAssociatedToChannel,
onSuccess: [GroupTypes.RECEIVED_GROUPS],
params: [
channelID,
q,
page,
perPage,
],
});
}
export function getAllGroupsAssociatedToTeam(teamID: string): ActionFunc {
return bindClientFunc({
clientFunc: async (param1) => {
const result = await Client4.getAllGroupsAssociatedToTeam(param1);
result.teamID = param1;
return result;
},
onSuccess: [GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_TEAM],
params: [
teamID,
],
});
}
export function getAllGroupsAssociatedToChannel(channelID: string): ActionFunc {
return bindClientFunc({
clientFunc: async (param1) => {
const result = await Client4.getAllGroupsAssociatedToChannel(param1);
result.channelID = param1;
return result;
},
onSuccess: [GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNEL],
params: [
channelID,
],
});
}
export function getGroupsAssociatedToTeam(teamID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc {
return bindClientFunc({
clientFunc: async (param1, param2, param3, param4) => {
const result = await Client4.getGroupsAssociatedToTeam(param1, param2, param3, param4);
return {groups: result.groups, totalGroupCount: result.total_group_count, teamID: param1};
},
onSuccess: [GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_TEAM],
params: [
teamID,
q,
page,
perPage,
],
});
}
export function getGroupsAssociatedToChannel(channelID: string, q = '', page = 0, perPage: number = General.PAGE_SIZE_DEFAULT): ActionFunc {
return bindClientFunc({
clientFunc: async (param1, param2, param3, param4) => {
const result = await Client4.getGroupsAssociatedToChannel(param1, param2, param3, param4);
return {groups: result.groups, totalGroupCount: result.total_group_count, channelID: param1};
},
onSuccess: [GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_CHANNEL],
params: [
channelID,
q,
page,
perPage,
],
});
}

View File

@@ -7,7 +7,6 @@ import * as errors from './errors';
import * as emojis from './emojis';
import * as files from './files';
import * as general from './general';
import * as groups from './groups';
import * as gifs from './gifs';
import * as helpers from './helpers';
import * as integrations from './integrations';
@@ -28,7 +27,6 @@ export {
emojis,
files,
general,
groups,
gifs,
integrations,
helpers,

View File

@@ -2847,6 +2847,13 @@ export default class Client4 {
);
};
getGroups = async (filterAllowReference = false) => {
return this.doFetch(
`${this.getBaseRoute()}/groups${buildQueryString({filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getGroupsNotAssociatedToTeam = async (teamID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT) => {
this.trackEvent('api', 'api_groups_get_not_associated_to_team', {team_id: teamID});
return this.doFetch(
@@ -2863,34 +2870,41 @@ export default class Client4 {
);
};
getGroupsAssociatedToTeam = async (teamID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT) => {
getGroupsAssociatedToTeam = async (teamID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT, filterAllowReference = false) => {
this.trackEvent('api', 'api_groups_get_associated_to_team', {team_id: teamID});
return this.doFetch(
`${this.getBaseRoute()}/teams/${teamID}/groups${buildQueryString({page, per_page: perPage, q, include_member_count: true})}`,
`${this.getBaseRoute()}/teams/${teamID}/groups${buildQueryString({page, per_page: perPage, q, include_member_count: true, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getGroupsAssociatedToChannel = async (channelID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT) => {
getGroupsAssociatedToChannel = async (channelID: string, q = '', page = 0, perPage = PER_PAGE_DEFAULT, filterAllowReference = false) => {
this.trackEvent('api', 'api_groups_get_associated_to_channel', {channel_id: channelID});
return this.doFetch(
`${this.getBaseRoute()}/channels/${channelID}/groups${buildQueryString({page, per_page: perPage, q, include_member_count: true})}`,
`${this.getBaseRoute()}/channels/${channelID}/groups${buildQueryString({page, per_page: perPage, q, include_member_count: true, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getAllGroupsAssociatedToTeam = async (teamID: string) => {
getAllGroupsAssociatedToTeam = async (teamID: string, filterAllowReference = false) => {
return this.doFetch(
`${this.getBaseRoute()}/teams/${teamID}/groups?paginate=false`,
`${this.getBaseRoute()}/teams/${teamID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getAllGroupsAssociatedToChannel = async (channelID: string) => {
getAllGroupsAssociatedToChannelsInTeam = async (teamID: string, filterAllowReference = false) => {
return this.doFetch(
`${this.getBaseRoute()}/channels/${channelID}/groups?paginate=false`,
`${this.getBaseRoute()}/teams/${teamID}/groups_by_channels${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getAllGroupsAssociatedToChannel = async (channelID: string, filterAllowReference = false) => {
return this.doFetch(
`${this.getBaseRoute()}/channels/${channelID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};

View File

@@ -547,6 +547,22 @@ function stats(state: RelationOneToOne<Channel, ChannelStats> = {}, action: Gene
function groupsAssociatedToChannel(state: any = {}, action: GenericAction) {
switch (action.type) {
case GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNELS_IN_TEAM: {
const {groupsByChannelId} = action.data;
const nextState = {...state};
for (const channelID of Object.keys(groupsByChannelId)) {
if (groupsByChannelId[channelID]) {
const associatedGroupIDs = new Set<string>([]);
for (const group of groupsByChannelId[channelID]) {
associatedGroupIDs.add(group.id);
}
const ids = Array.from(associatedGroupIDs);
nextState[channelID] = {ids, totalCount: ids.length};
}
}
return nextState;
}
case GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_CHANNEL: {
const {channelID, groups, totalGroupCount} = action.data;
const nextState = {...state};

View File

@@ -194,7 +194,20 @@ function groups(state: Dictionary<Group> = {}, action: GenericAction) {
}
return nextState;
}
case GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNELS_IN_TEAM: {
const nextState = {...state};
const {groupsByChannelId} = action.data;
for (const group of Object.values(groupsByChannelId) as Group[]) {
if (group) {
nextState[group.id] = group;
}
}
return nextState;
}
case GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_TEAM:
case GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_TEAM:
case GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNEL:
case GroupTypes.RECEIVED_GROUPS_ASSOCIATED_TO_CHANNEL: {
const nextState = {...state};
for (const group of action.data.groups) {

View File

@@ -34,6 +34,7 @@ describe('Selectors.Groups', () => {
delete_at: 0,
has_syncables: false,
member_count: 2,
allow_reference: true,
},
[expectedAssociatedGroupID3]: {
id: expectedAssociatedGroupID3,
@@ -47,9 +48,10 @@ describe('Selectors.Groups', () => {
delete_at: 0,
has_syncables: false,
member_count: 5,
allow_reference: false,
},
[expectedAssociatedGroupID4]: {
id: [expectedAssociatedGroupID4],
id: expectedAssociatedGroupID4,
name: 'nobctj4brfgtpj3a1peiyq47tc',
display_name: 'engineering',
description: '',
@@ -60,6 +62,7 @@ describe('Selectors.Groups', () => {
delete_at: 0,
has_syncables: false,
member_count: 8,
allow_reference: true,
},
[expectedAssociatedGroupID2]: {
id: expectedAssociatedGroupID2,
@@ -73,6 +76,7 @@ describe('Selectors.Groups', () => {
delete_at: 0,
has_syncables: false,
member_count: 2,
allow_reference: false,
},
},
},
@@ -114,4 +118,26 @@ describe('Selectors.Groups', () => {
const expected = Object.entries(testState.entities.groups.groups).filter(([groupID]) => !channelAssociatedGroupIDs.includes(groupID)).map(([, group]) => group);
assert.deepEqual(Selectors.getGroupsNotAssociatedToChannel(testState, channelID), expected);
});
it('getGroupsAssociatedToTeamForReference', () => {
const expected = [
testState.entities.groups.groups[expectedAssociatedGroupID1],
];
assert.deepEqual(Selectors.getGroupsAssociatedToTeamForReference(testState, teamID), expected);
});
it('getGroupsAssociatedToChannelForReference', () => {
const expected = [
testState.entities.groups.groups[expectedAssociatedGroupID4],
];
assert.deepEqual(Selectors.getGroupsAssociatedToChannelForReference(testState, channelID), expected);
});
it('getAllAssociatedGroupsForReference', () => {
const expected = [
testState.entities.groups.groups[expectedAssociatedGroupID1],
testState.entities.groups.groups[expectedAssociatedGroupID4],
];
assert.deepEqual(Selectors.getAllAssociatedGroupsForReference(testState, channelID), expected);
});
});

View File

@@ -2,6 +2,14 @@
// See LICENSE.txt for license information.
import * as reselect from 'reselect';
import {GlobalState} from '@mm-redux/types/store';
import {Group} from '@mm-redux/types/groups';
import {filterGroupsMatchingTerm} from '@mm-redux/utils/group_utils';
import {getCurrentUserLocale} from '@mm-redux/selectors/entities/i18n';
import {getChannel} from '@mm-redux/selectors/entities/channels';
import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles';
import {getTeam} from '@mm-redux/selectors/entities/teams';
import {Permissions} from '@mm-redux/constants';
const emptyList: any[] = [];
const emptySyncables = {
teams: [],
@@ -9,7 +17,7 @@ const emptySyncables = {
};
export function getAllGroups(state: GlobalState) {
return state.entities.groups.groups;
return state.entities.groups?.groups || [];
}
export function getGroup(state: GlobalState, id: string) {
@@ -45,6 +53,44 @@ export function getGroupMembers(state: GlobalState, id: string) {
return groupMemberData.members;
}
export function searchAssociatedGroupsForReferenceLocal(state: GlobalState, term: string, teamId: string, channelId: string): Array<Group> {
const groups = getAssociatedGroupsForReference(state, teamId, channelId);
if (!groups || groups.length === 0) {
return emptyList;
}
const filteredGroups = filterGroupsMatchingTerm(groups, term);
return filteredGroups;
}
export function getAssociatedGroupsForReference(state: GlobalState, teamId: string, channelId: string): Array<Group> {
const team = getTeam(state, teamId);
const channel = getChannel(state, channelId);
const locale = getCurrentUserLocale(state);
if (!haveIChannelPermission(state, {
permission: Permissions.USE_GROUP_MENTIONS,
channel: channelId,
team: teamId,
default: true,
})) {
return emptyList;
}
let groupsForReference = [];
if (team && team.group_constrained && channel && channel.group_constrained) {
const groupsFromChannel = getGroupsAssociatedToChannelForReference(state, channelId);
const groupsFromTeam = getGroupsAssociatedToTeamForReference(state, teamId);
groupsForReference = groupsFromChannel.concat(groupsFromTeam.filter((item) => groupsFromChannel.indexOf(item) < 0));
} else if (team && team.group_constrained) {
groupsForReference = getGroupsAssociatedToTeamForReference(state, teamId);
} else if (channel && channel.group_constrained) {
groupsForReference = getGroupsAssociatedToChannelForReference(state, channelId);
} else {
groupsForReference = getAllAssociatedGroupsForReference(state);
}
return groupsForReference.sort((groupA: Group, groupB: Group) => groupA.name.localeCompare(groupB.name, locale));
}
const teamGroupIDs = (state: GlobalState, teamID: string) => state.entities.teams.groupsAssociatedToTeam[teamID]?.ids || [];
const channelGroupIDs = (state: GlobalState, channelID: string) => state.entities.channels.groupsAssociatedToChannel[channelID]?.ids || [];
@@ -63,7 +109,7 @@ export const getGroupsNotAssociatedToTeam = reselect.createSelector(
getAllGroups,
(state: GlobalState, teamID: string) => getTeamGroupIDSet(state, teamID),
(allGroups, teamGroupIDSet) => {
return Object.entries(allGroups).filter(([groupID]) => !teamGroupIDSet.has(groupID)).map((entry) => entry[1]);
return Object.values(allGroups).filter((group) => !teamGroupIDSet.has(group.id));
},
);
@@ -71,7 +117,7 @@ export const getGroupsAssociatedToTeam = reselect.createSelector(
getAllGroups,
(state: GlobalState, teamID: string) => getTeamGroupIDSet(state, teamID),
(allGroups, teamGroupIDSet) => {
return Object.entries(allGroups).filter(([groupID]) => teamGroupIDSet.has(groupID)).map((entry) => entry[1]);
return Object.values(allGroups).filter((group) => teamGroupIDSet.has(group.id));
},
);
@@ -79,7 +125,7 @@ export const getGroupsNotAssociatedToChannel = reselect.createSelector(
getAllGroups,
(state: GlobalState, channelID: string) => getChannelGroupIDSet(state, channelID),
(allGroups, channelGroupIDSet) => {
return Object.entries(allGroups).filter(([groupID]) => !channelGroupIDSet.has(groupID)).map((entry) => entry[1]);
return Object.values(allGroups).filter((group) => !channelGroupIDSet.has(group.id));
},
);
@@ -87,6 +133,29 @@ export const getGroupsAssociatedToChannel = reselect.createSelector(
getAllGroups,
(state: GlobalState, channelID: string) => getChannelGroupIDSet(state, channelID),
(allGroups, channelGroupIDSet) => {
return Object.entries(allGroups).filter(([groupID]) => channelGroupIDSet.has(groupID)).map((entry) => entry[1]);
return Object.values(allGroups).filter((group) => channelGroupIDSet.has(group.id));
},
);
export const getGroupsAssociatedToTeamForReference = reselect.createSelector(
getAllGroups,
(state: GlobalState, teamID: string) => getTeamGroupIDSet(state, teamID),
(allGroups, teamGroupIDSet) => {
return Object.values(allGroups).filter((group) => teamGroupIDSet.has(group.id) && group.allow_reference && group.delete_at === 0);
},
);
export const getGroupsAssociatedToChannelForReference = reselect.createSelector(
getAllGroups,
(state: GlobalState, channelID: string) => getChannelGroupIDSet(state, channelID),
(allGroups, channelGroupIDSet) => {
return Object.values(allGroups).filter((group) => channelGroupIDSet.has(group.id) && group.allow_reference && group.delete_at === 0);
},
);
export const getAllAssociatedGroupsForReference = reselect.createSelector(
getAllGroups,
(allGroups) => {
return Object.values(allGroups).filter((group) => group.allow_reference && group.delete_at === 0);
},
);

View File

@@ -18,6 +18,7 @@ export type Group = {
has_syncables: boolean;
member_count: number;
scheme_admin: boolean;
allow_reference: boolean;
};
export type GroupTeam = {
team_id: string;

View File

@@ -0,0 +1,92 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import assert from 'assert';
import {
filterGroupsMatchingTerm,
} from './group_utils';
describe('group utils', () => {
describe('filterGroupsMatchingTerm', () => {
const groupA = {
id: 'groupid1',
name: 'board-group',
description: 'group1 description',
display_name: 'board-group',
type: 'ldap',
remote_id: 'group1',
create_at: 1,
update_at: 2,
delete_at: 0,
has_syncables: true,
member_count: 3,
scheme_admin: false,
allow_reference: true,
};
const groupB = {
id: 'groupid2',
name: 'developers-group',
description: 'group2 description',
display_name: 'developers-group',
type: 'ldap',
remote_id: 'group2',
create_at: 1,
update_at: 2,
delete_at: 0,
has_syncables: true,
member_count: 3,
scheme_admin: false,
allow_reference: true,
};
const groupC = {
id: 'groupid3',
name: 'software-engineers',
description: 'group3 description',
display_name: 'software engineers',
type: 'ldap',
remote_id: 'group3',
create_at: 1,
update_at: 2,
delete_at: 0,
has_syncables: true,
member_count: 3,
scheme_admin: false,
allow_reference: true,
};
const groups = [groupA, groupB, groupC];
it('should match all for empty filter', () => {
assert.deepEqual(filterGroupsMatchingTerm(groups, ''), [groupA, groupB, groupC]);
});
it('should filter out results which do not match', () => {
assert.deepEqual(filterGroupsMatchingTerm(groups, 'testBad'), []);
});
it('should match by name', () => {
assert.deepEqual(filterGroupsMatchingTerm(groups, 'software-engineers'), [groupC]);
});
it('should match by split part of the name', () => {
assert.deepEqual(filterGroupsMatchingTerm(groups, 'group'), [groupA, groupB]);
assert.deepEqual(filterGroupsMatchingTerm(groups, 'board'), [groupA]);
});
it('should match by display_name fully', () => {
assert.deepEqual(filterGroupsMatchingTerm(groups, 'software engineers'), [groupC]);
});
it('should match by display_name case-insensitive', () => {
assert.deepEqual(filterGroupsMatchingTerm(groups, 'software ENGINEERS'), [groupC]);
});
it('should ignore leading @ for name', () => {
assert.deepEqual(filterGroupsMatchingTerm(groups, '@developers'), [groupB]);
});
it('should ignore leading @ for display_name', () => {
assert.deepEqual(filterGroupsMatchingTerm(groups, '@software'), [groupC]);
});
});
});

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {General} from '../constants';
import {Group} from '@mm-redux/types/groups';
import {getSuggestionsSplitByMultiple} from './user_utils';
export function filterGroupsMatchingTerm(groups: Array<Group>, term: string): Array<Group> {
const lowercasedTerm = term.toLowerCase();
let trimmedTerm = lowercasedTerm;
if (trimmedTerm.startsWith('@')) {
trimmedTerm = trimmedTerm.substr(1);
}
if (!trimmedTerm) {
return groups;
}
return groups.filter((group: Group) => {
if (!group) {
return false;
}
const groupSuggestions: string[] = [];
const groupnameSuggestions = getSuggestionsSplitByMultiple((group.name || '').toLowerCase(), General.AUTOCOMPLETE_SPLIT_CHARACTERS);
groupSuggestions.push(...groupnameSuggestions);
const displayname = (group.display_name || '').toLowerCase();
groupSuggestions.push(displayname);
return groupSuggestions.
some((suggestion) => suggestion.startsWith(trimmedTerm));
});
}

View File

@@ -571,6 +571,7 @@
"suggestion.mention.all": "Notifies everyone in this channel",
"suggestion.mention.channel": "Notifies everyone in this channel",
"suggestion.mention.channels": "My Channels",
"suggestion.mention.groups": "Group Mentions",
"suggestion.mention.here": "Notifies everyone online in this channel",
"suggestion.mention.members": "Channel Members",
"suggestion.mention.morechannels": "Other Channels",