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>
Этот коммит содержится в:
Scott Bishel
2024-06-25 12:25:28 -06:00
коммит произвёл GitHub
родитель 4f68dbb96e
Коммит 6fd894953c
13 изменённых файлов: 408 добавлений и 103 удалений

Просмотреть файл

@@ -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});