update for adding multiple members (#25128)
* update for adding multiple members
* fix unit test
* more test fixes
* add another unit test
* fix object passed by client4
* revert package-lock.json
* revert package-lock.json
* add length check
* limit size of lists in API requests
* revert package-lock
* add batching to front end
* add batching to front end
* fix bad merge
* update return type
* remove unnecessary permisssion check, add unit test
* fixes and add tests from review
* revert changes adding limits to other apis
* fixes
* clean-up from code review
* fix unit test call
* revert back to interface{}, fix unit test
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
4f68dbb96e
Коммит
6fd894953c
@@ -117,7 +117,7 @@ jest.mock('mattermost-redux/actions/channels', () => ({
|
||||
}],
|
||||
};
|
||||
},
|
||||
addChannelMember: (...args: any) => ({type: 'MOCK_ADD_CHANNEL_MEMBER', args}),
|
||||
addChannelMembers: (...args: any) => ({type: 'MOCK_ADD_CHANNEL_MEMBERS', args}),
|
||||
createDirectChannel: (...args: any) => ({type: 'MOCK_CREATE_DIRECT_CHANNEL', args}),
|
||||
createGroupChannel: (...args: any) => ({type: 'MOCK_CREATE_GROUP_CHANNEL', args}),
|
||||
}));
|
||||
@@ -146,13 +146,13 @@ describe('Actions.Channel', () => {
|
||||
const testStore = await mockStore(initialState);
|
||||
|
||||
const expectedActions = [{
|
||||
type: 'MOCK_ADD_CHANNEL_MEMBER',
|
||||
args: ['testid', 'testuserid'],
|
||||
type: 'MOCK_ADD_CHANNEL_MEMBERS',
|
||||
args: ['testid', ['testuserid', 'testuserid2']],
|
||||
}];
|
||||
|
||||
const fakeData = {
|
||||
channel: 'testid',
|
||||
userIds: ['testuserid'],
|
||||
userIds: ['testuserid', 'testuserid2'],
|
||||
};
|
||||
|
||||
await testStore.dispatch(addUsersToChannel(fakeData.channel, fakeData.userIds));
|
||||
|
||||
@@ -155,15 +155,11 @@ export function autocompleteChannelsForSearch(term: string, success?: (channels:
|
||||
|
||||
export function addUsersToChannel(channelId: Channel['id'], userIds: Array<UserProfile['id']>): ActionFuncAsync {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const requests = userIds.map((uId) => dispatch(ChannelActions.addChannelMember(channelId, uId)));
|
||||
|
||||
await Promise.all(requests);
|
||||
|
||||
return {data: true};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
const error = await dispatch(ChannelActions.addChannelMembers(channelId, userIds));
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1347,6 +1347,90 @@ describe('Actions.Channels', () => {
|
||||
expect(stats[channelId].member_count >= 2).toBeTruthy();
|
||||
});
|
||||
|
||||
it('addChannelMembers', async () => {
|
||||
const channelId = TestHelper.basicChannel!.id;
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
post(`/channels/${TestHelper.basicChannel!.id}/members`).
|
||||
reply(201, {channel_id: TestHelper.basicChannel!.id, roles: 'channel_user', user_id: TestHelper.basicUser!.id});
|
||||
|
||||
await store.dispatch(Actions.joinChannel(TestHelper.basicUser!.id, TestHelper.basicTeam!.id, channelId));
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
get(`/channels/${TestHelper.basicChannel!.id}/stats?exclude_files_count=true`).
|
||||
reply(200, {channel_id: TestHelper.basicChannel!.id, member_count: 1});
|
||||
|
||||
await store.dispatch(Actions.getChannelStats(channelId));
|
||||
|
||||
let state = store.getState();
|
||||
let {stats} = state.entities.channels;
|
||||
expect(stats).toBeTruthy();
|
||||
|
||||
// stats for channel
|
||||
expect(stats[channelId]).toBeTruthy();
|
||||
|
||||
// member count for channel
|
||||
expect(stats[channelId].member_count).toBeTruthy();
|
||||
|
||||
// incorrect member count for channel
|
||||
expect(stats[channelId].member_count >= 1).toBeTruthy();
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
post('/users').
|
||||
query(true).
|
||||
reply(201, TestHelper.fakeUserWithId());
|
||||
const user = await TestHelper.basicClient4!.createUser(
|
||||
TestHelper.fakeUser(),
|
||||
'',
|
||||
'',
|
||||
TestHelper.basicTeam!.invite_id,
|
||||
);
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
post('/users').
|
||||
query(true).
|
||||
reply(201, TestHelper.fakeUserWithId());
|
||||
const user2 = await TestHelper.basicClient4!.createUser(
|
||||
TestHelper.fakeUser(),
|
||||
'',
|
||||
'',
|
||||
TestHelper.basicTeam!.invite_id,
|
||||
);
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
post(`/channels/${TestHelper.basicChannel!.id}/members`).
|
||||
reply(201, [{channel_id: TestHelper.basicChannel!.id, roles: 'channel_user', user_id: user.id},
|
||||
{channel_id: TestHelper.basicChannel!.id, roles: 'channel_user', user_id: user2.id}]);
|
||||
|
||||
await store.dispatch(Actions.addChannelMembers(channelId, [user.id, user2.id]));
|
||||
|
||||
state = store.getState();
|
||||
|
||||
const {profilesInChannel, profilesNotInChannel} = state.entities.users;
|
||||
const channel = profilesInChannel[channelId];
|
||||
const notChannel = profilesNotInChannel[channelId];
|
||||
expect(channel).toBeTruthy();
|
||||
expect(notChannel).toBeTruthy();
|
||||
expect(channel.has(user.id)).toBeTruthy();
|
||||
expect(channel.has(user2.id)).toBeTruthy();
|
||||
|
||||
// user should not present in profilesNotInChannel
|
||||
expect(notChannel.has(user.id)).toEqual(false);
|
||||
expect(notChannel.has(user2.id)).toEqual(false);
|
||||
|
||||
stats = state.entities.channels.stats;
|
||||
expect(stats).toBeTruthy();
|
||||
|
||||
// stats for channel
|
||||
expect(stats[channelId]).toBeTruthy();
|
||||
|
||||
// member count for channel
|
||||
expect(stats[channelId].member_count).toBeTruthy();
|
||||
|
||||
// incorrect member count for channel
|
||||
expect(stats[channelId].member_count >= 3).toBeTruthy();
|
||||
});
|
||||
|
||||
it('removeChannelMember', async () => {
|
||||
const channelId = TestHelper.basicChannel!.id;
|
||||
|
||||
|
||||
@@ -1048,6 +1048,45 @@ export function addChannelMember(channelId: string, userId: string, postRootId =
|
||||
};
|
||||
}
|
||||
|
||||
export function addChannelMembers(channelId: string, userIds: string[], postRootId = ''): ActionFuncAsync {
|
||||
const batchSize = 1000;
|
||||
return async (dispatch, getState) => {
|
||||
const channelMembers: ChannelMembership[] = [];
|
||||
try {
|
||||
for (let i = 0; i < userIds.length; i += batchSize) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const cm = await Client4.addToChannels(userIds.slice(i, i + batchSize), channelId, postRootId);
|
||||
channelMembers.push(...cm);
|
||||
}
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
Client4.trackEvent('action', 'action_channels_add_member', {channel_id: channelId});
|
||||
|
||||
const ids = channelMembers.map((member) => ({id: member.user_id}));
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: UserTypes.RECEIVED_PROFILES_IN_CHANNEL,
|
||||
id: channelId,
|
||||
data: ids,
|
||||
},
|
||||
{
|
||||
type: ChannelTypes.RECEIVED_CHANNEL_MEMBERS,
|
||||
data: channelMembers,
|
||||
},
|
||||
{
|
||||
type: ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS,
|
||||
id: channelId,
|
||||
count: channelMembers.length,
|
||||
},
|
||||
], 'ADD_CHANNEL_MEMBERS.BATCH'));
|
||||
return {data: channelMembers};
|
||||
};
|
||||
}
|
||||
|
||||
export function removeChannelMember(channelId: string, userId: string): ActionFuncAsync {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
@@ -1400,6 +1439,7 @@ export default {
|
||||
searchGroupChannels,
|
||||
getChannelStats,
|
||||
addChannelMember,
|
||||
addChannelMembers,
|
||||
removeChannelMember,
|
||||
markChannelAsRead,
|
||||
favoriteChannel,
|
||||
|
||||
@@ -135,6 +135,36 @@ describe('channels', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ADD_CHANNEL_MEMBER_SUCCESS', () => {
|
||||
const state = deepFreeze(channelsReducer({
|
||||
stats: {
|
||||
channel1: {
|
||||
id: 'channel1',
|
||||
member_count: 1,
|
||||
},
|
||||
},
|
||||
}, {}));
|
||||
test('should increment by 1 default', () => {
|
||||
const nextState = channelsReducer(state, {
|
||||
type: ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS,
|
||||
id: 'channel1',
|
||||
});
|
||||
|
||||
expect(nextState).not.toBe(state);
|
||||
expect(nextState.stats.channel1.member_count).toEqual(2);
|
||||
});
|
||||
test('should increment by number passed', () => {
|
||||
const nextState = channelsReducer(state, {
|
||||
type: ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS,
|
||||
id: 'channel1',
|
||||
count: 100,
|
||||
});
|
||||
|
||||
expect(nextState).not.toBe(state);
|
||||
expect(nextState.stats.channel1.member_count).toEqual(101);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REMOVE_MEMBER_FROM_CHANNEL', () => {
|
||||
test('should remove the channel member', () => {
|
||||
const state = deepFreeze(channelsReducer({
|
||||
|
||||
@@ -592,9 +592,10 @@ function stats(state: RelationOneToOne<Channel, ChannelStats> = {}, action: AnyA
|
||||
case ChannelTypes.ADD_CHANNEL_MEMBER_SUCCESS: {
|
||||
const nextState = {...state};
|
||||
const id = action.id;
|
||||
const receivedCount = action.count ? action.count : 1;
|
||||
const nextStat = nextState[id];
|
||||
if (nextStat) {
|
||||
const count = nextStat.member_count + 1;
|
||||
const count = nextStat.member_count + receivedCount;
|
||||
return {
|
||||
...nextState,
|
||||
[id]: {
|
||||
|
||||
@@ -304,6 +304,34 @@ describe('Reducers.users', () => {
|
||||
expect(newState.profilesNotInChannel).toEqual(expectedState.profilesNotInChannel);
|
||||
});
|
||||
|
||||
it('UserTypes.RECEIVED_PROFILES_IN_CHANNEL, existing state', () => {
|
||||
const state = {
|
||||
profilesNotInChannel: {
|
||||
id: new Set().add('old_user_id').add('other_user_id'),
|
||||
},
|
||||
};
|
||||
const action = {
|
||||
type: UserTypes.RECEIVED_PROFILES_IN_CHANNEL,
|
||||
id: 'id',
|
||||
data: {
|
||||
old_user_id: {
|
||||
id: 'old_user_id',
|
||||
},
|
||||
other_user_id: {
|
||||
id: 'other_user_id',
|
||||
},
|
||||
},
|
||||
};
|
||||
const expectedState = {
|
||||
profilesNotInChannel: {
|
||||
id: new Set(),
|
||||
},
|
||||
};
|
||||
|
||||
const newState = reducer(state as unknown as ReducerState, action);
|
||||
expect(newState.profilesNotInChannel).toEqual(expectedState.profilesNotInChannel);
|
||||
});
|
||||
|
||||
it('UserTypes.RECEIVED_PROFILE_NOT_IN_CHANNEL, no existing profiles', () => {
|
||||
const state = {
|
||||
profilesNotInChannel: {},
|
||||
|
||||
@@ -18,6 +18,12 @@ function profilesToSet(state: RelationOneToManyUnique<Team, UserProfile>, action
|
||||
return users.reduce((nextState, user) => addProfileToSet(nextState, id, user.id), state);
|
||||
}
|
||||
|
||||
function removeProfilesFromSet(state: RelationOneToManyUnique<Team, UserProfile>, action: AnyAction) {
|
||||
const id = action.id;
|
||||
const users: UserProfile[] = Object.values(action.data);
|
||||
return users.reduce((nextState, user) => removeProfileFromSet(nextState, {type: '', data: {id, user_id: user.id}}), state);
|
||||
}
|
||||
|
||||
function profileListToSet(state: RelationOneToManyUnique<Team, UserProfile>, action: AnyAction, replace = false) {
|
||||
const id = action.id;
|
||||
const users: UserProfile[] = action.data || [];
|
||||
@@ -391,6 +397,9 @@ function profilesNotInChannel(state: UsersState['profilesNotInChannel'] = {}, ac
|
||||
case UserTypes.RECEIVED_PROFILES_NOT_IN_CHANNEL:
|
||||
return profilesToSet(state, action);
|
||||
|
||||
case UserTypes.RECEIVED_PROFILES_IN_CHANNEL:
|
||||
return removeProfilesFromSet(state, action);
|
||||
|
||||
case UserTypes.RECEIVED_PROFILE_IN_CHANNEL:
|
||||
return removeProfileFromSet(state, action);
|
||||
|
||||
|
||||
@@ -1811,6 +1811,16 @@ export default class Client4 {
|
||||
);
|
||||
};
|
||||
|
||||
addToChannels = (userIds: string[], channelId: string, postRootId = '') => {
|
||||
this.trackEvent('api', 'api_channels_add_members', {channel_id: channelId});
|
||||
|
||||
const members = {user_ids: userIds, channel_id: channelId, post_root_id: postRootId};
|
||||
return this.doFetch<ChannelMembership[]>(
|
||||
`${this.getChannelMembersRoute(channelId)}`,
|
||||
{method: 'post', body: JSON.stringify(members)},
|
||||
);
|
||||
};
|
||||
|
||||
addToChannel = (userId: string, channelId: string, postRootId = '') => {
|
||||
this.trackEvent('api', 'api_channels_add_member', {channel_id: channelId});
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user