MM-54123 Add group to channel (#22954)
* adding group members to channel initial commit * adding group to channel functionality along with add new team members * fixing circular dependency * fixing e2e and other optimizations * adding e2e tests for adding group members to channels * cypress lint * fixing comments * adding count to button * improvements * adjusting some stuff from PR comments * remove ability to add user to team, update message for non-team members * remove adding to team from add groups functionality * update misspelled variable * lint and unit test fixes * add tests, cleanup * lint fix * revert package-lock.json * fixes for cypress tests * rename TeamInviteBanner to TeamWarningBanner, since invites are no longer allowed * update for warning * lint fixes * cleanup * fix failing e2e tests * update messages to not use markdown --------- Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -10,29 +10,53 @@
|
|||||||
// Stage: @prod
|
// Stage: @prod
|
||||||
// Group: @channels @channel @channel_settings @smoke
|
// Group: @channels @channel @channel_settings @smoke
|
||||||
|
|
||||||
|
import {getRandomId} from '../../../utils';
|
||||||
|
|
||||||
describe('Channel Settings', () => {
|
describe('Channel Settings', () => {
|
||||||
let testTeam: Cypress.Team;
|
let testTeam: Cypress.Team;
|
||||||
let firstUser: Cypress.UserProfile;
|
let firstUser: Cypress.UserProfile;
|
||||||
let addedUsersChannel: Cypress.Channel;
|
let addedUsersChannel: Cypress.Channel;
|
||||||
let username: string;
|
let username: string;
|
||||||
|
let groupId: string;
|
||||||
const usernames: string[] = [];
|
const usernames: string[] = [];
|
||||||
|
|
||||||
|
const users: Cypress.UserProfile[] = [];
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.apiInitSetup().then(({team, user}) => {
|
cy.apiInitSetup().then(({team, user}) => {
|
||||||
testTeam = team;
|
testTeam = team;
|
||||||
firstUser = user;
|
firstUser = user;
|
||||||
|
const teamId = testTeam.id;
|
||||||
|
|
||||||
// # Add 4 users
|
// # Add 10 users
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
cy.apiCreateUser().then(({user: newUser}) => { // eslint-disable-line
|
cy.apiCreateUser().then(({user: newUser}) => {
|
||||||
cy.apiAddUserToTeam(testTeam.id, newUser.id);
|
users.push(newUser);
|
||||||
|
cy.apiAddUserToTeam(teamId, newUser.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => {
|
cy.apiCreateChannel(teamId, 'channel-test', 'Channel').then(({channel}) => {
|
||||||
addedUsersChannel = channel;
|
addedUsersChannel = channel;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// # Change permission so that regular users can't add team members
|
||||||
|
cy.apiGetRolesByNames(['team_user']).then((result: any) => {
|
||||||
|
if (result.roles) {
|
||||||
|
const role = result.roles[0];
|
||||||
|
const permissions = role.permissions.filter((permission) => {
|
||||||
|
return !(['add_user_to_team'].includes(permission));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (permissions.length !== role.permissions) {
|
||||||
|
cy.apiPatchRole(role.id, {permissions});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
cy.apiLogin(firstUser);
|
cy.apiLogin(firstUser);
|
||||||
|
}).then(() => {
|
||||||
|
groupId = getRandomId();
|
||||||
|
cy.apiCreateCustomUserGroup(`group${groupId}`, `group${groupId}`, [users[0].id, users[1].id]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,7 +66,7 @@ describe('Channel Settings', () => {
|
|||||||
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
|
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
|
||||||
|
|
||||||
// # Add users to channel
|
// # Add users to channel
|
||||||
addNumberOfUsersToChannel(1);
|
addNumberOfUsersToChannel(1, false);
|
||||||
|
|
||||||
cy.getLastPostId().then((id) => {
|
cy.getLastPostId().then((id) => {
|
||||||
// * The system message should contain 'added to the channel by you'
|
// * The system message should contain 'added to the channel by you'
|
||||||
@@ -59,7 +83,7 @@ describe('Channel Settings', () => {
|
|||||||
cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => {
|
cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => {
|
||||||
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
|
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
|
||||||
|
|
||||||
addNumberOfUsersToChannel(3);
|
addNumberOfUsersToChannel(3, false);
|
||||||
|
|
||||||
cy.getLastPostId().then((id) => {
|
cy.getLastPostId().then((id) => {
|
||||||
cy.get(`#postMessageText_${id}`).should('contain', '2 others were added to the channel by you');
|
cy.get(`#postMessageText_${id}`).should('contain', '2 others were added to the channel by you');
|
||||||
@@ -82,10 +106,10 @@ describe('Channel Settings', () => {
|
|||||||
cy.get('#addUsersToChannelModal').should('be.visible');
|
cy.get('#addUsersToChannelModal').should('be.visible');
|
||||||
|
|
||||||
// # Type into the input box to search for a user
|
// # Type into the input box to search for a user
|
||||||
cy.get('#selectItems input').typeWithForce('u');
|
cy.get('#selectItems input').typeWithForce('user');
|
||||||
|
|
||||||
// # First add one user in order to see them disappearing from the list
|
// # First add one user in order to see them disappearing from the list
|
||||||
cy.get('#multiSelectList > div').first().then((el) => {
|
cy.get('#multiSelectList > div').not(':contains("Already in channel")').first().then((el) => {
|
||||||
const childNodes = Array.from(el[0].childNodes);
|
const childNodes = Array.from(el[0].childNodes);
|
||||||
childNodes.map((child: HTMLElement) => usernames.push(child.innerText));
|
childNodes.map((child: HTMLElement) => usernames.push(child.innerText));
|
||||||
|
|
||||||
@@ -113,7 +137,7 @@ describe('Channel Settings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add two more users
|
// Add two more users
|
||||||
addNumberOfUsersToChannel(2);
|
addNumberOfUsersToChannel(2, false);
|
||||||
|
|
||||||
// Verify that the system post reflects the number of added users
|
// Verify that the system post reflects the number of added users
|
||||||
cy.getLastPostId().then((id) => {
|
cy.getLastPostId().then((id) => {
|
||||||
@@ -148,6 +172,179 @@ describe('Channel Settings', () => {
|
|||||||
});
|
});
|
||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Add group members to channel', () => {
|
||||||
|
cy.apiLogin(firstUser);
|
||||||
|
|
||||||
|
// # Create a new channel
|
||||||
|
cy.apiCreateChannel(testTeam.id, 'new-channel', 'New Channel').then(({channel}) => {
|
||||||
|
// # Visit the channel
|
||||||
|
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
|
||||||
|
|
||||||
|
// # Open channel menu and click 'Add Members'
|
||||||
|
cy.uiOpenChannelMenu('Add Members');
|
||||||
|
|
||||||
|
// * Assert that modal appears
|
||||||
|
cy.get('#addUsersToChannelModal').should('be.visible');
|
||||||
|
|
||||||
|
// # Type 'group'+ id created in beforeAll into the input box
|
||||||
|
cy.get('#selectItems input').typeWithForce(`group${groupId}`);
|
||||||
|
|
||||||
|
// # Click the first row for a number of times
|
||||||
|
// cy.get('#multiSelectList').should('be.visible').first().click();
|
||||||
|
cy.get('#multiSelectList').should('exist').children().first().click();
|
||||||
|
|
||||||
|
// # Click the button "Add" to add user to a channel
|
||||||
|
cy.uiGetButton('Add').click();
|
||||||
|
|
||||||
|
// # Wait for the modal to disappear
|
||||||
|
cy.get('#addUsersToChannelModal').should('not.exist');
|
||||||
|
|
||||||
|
cy.getLastPostId().then((id) => {
|
||||||
|
// * The system message should contain 'added to the channel by you'
|
||||||
|
cy.get(`#postMessageText_${id}`).should('contain', 'added to the channel by you');
|
||||||
|
|
||||||
|
// # Verify username link
|
||||||
|
verifyMentionedUserAndProfilePopover(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// * Check that the number of channel members is 3
|
||||||
|
cy.get('#channelMemberCountText').
|
||||||
|
should('be.visible').
|
||||||
|
and('have.text', '3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add group members that are not team members', () => {
|
||||||
|
cy.apiAdminLogin();
|
||||||
|
|
||||||
|
// # Create a new user
|
||||||
|
cy.apiCreateUser().then(({user: newUser}) => {
|
||||||
|
const id = getRandomId();
|
||||||
|
|
||||||
|
// # Create a custom user group
|
||||||
|
cy.apiCreateCustomUserGroup(`newgroup${id}`, `newgroup${id}`, [newUser.id]).then(() => {
|
||||||
|
// # Create a new channel
|
||||||
|
cy.apiCreateChannel(testTeam.id, 'new-group-channel', 'New Group Channel').then(({channel}) => {
|
||||||
|
// # Visit a channel
|
||||||
|
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
|
||||||
|
|
||||||
|
// # Open channel menu and click 'Add Members'
|
||||||
|
cy.uiOpenChannelMenu('Add Members');
|
||||||
|
|
||||||
|
// * Assert that modal appears
|
||||||
|
cy.get('#addUsersToChannelModal').should('be.visible');
|
||||||
|
|
||||||
|
// # Type 'group' into the input box
|
||||||
|
cy.get('#selectItems input').typeWithForce(`newgroup${id}`);
|
||||||
|
|
||||||
|
// # Click the first row for a number of times
|
||||||
|
// cy.get('#multiSelectList').should('be.visible').first().click();
|
||||||
|
cy.get('#multiSelectList').should('exist').children().first().click();
|
||||||
|
|
||||||
|
// * Check you get a warning when adding a non team member
|
||||||
|
cy.findByTestId('teamWarningBanner').should('contain', '1 user was not selected because they are not a part of this team');
|
||||||
|
|
||||||
|
// * Check the correct username is appearing in the team invite banner
|
||||||
|
cy.findByTestId('teamWarningBanner').should('contain', `@${newUser.username}`);
|
||||||
|
|
||||||
|
// # Click the button "Add" to add user to a channel
|
||||||
|
cy.uiGetButton('Cancel').click();
|
||||||
|
|
||||||
|
// # Wait for the modal to disappear
|
||||||
|
cy.get('#addUsersToChannelModal').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add group members and guests that are not team members', () => {
|
||||||
|
cy.apiAdminLogin();
|
||||||
|
|
||||||
|
// # Create a new user
|
||||||
|
cy.apiCreateUser().then(({user: newUser}) => {
|
||||||
|
// # Create a guest user
|
||||||
|
cy.apiCreateGuestUser({}).then(({guest}) => {
|
||||||
|
const id = getRandomId();
|
||||||
|
|
||||||
|
// # Create a custom user group
|
||||||
|
cy.apiCreateCustomUserGroup(`guestgroup${id}`, `guestgroup${id}`, [guest.id, newUser.id]).then(() => {
|
||||||
|
// # Create a new channel
|
||||||
|
cy.apiCreateChannel(testTeam.id, 'group-guest-channel', 'Channel').then(({channel}) => {
|
||||||
|
// # Visit a channel
|
||||||
|
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
|
||||||
|
|
||||||
|
// # Open channel menu and click 'Add Members'
|
||||||
|
cy.uiOpenChannelMenu('Add Members');
|
||||||
|
|
||||||
|
// * Assert that modal appears
|
||||||
|
cy.get('#addUsersToChannelModal').should('be.visible');
|
||||||
|
|
||||||
|
// # Type 'group' into the input box
|
||||||
|
cy.get('#selectItems input').typeWithForce(`guestgroup${id}`);
|
||||||
|
|
||||||
|
// # Click the first row for a number of times
|
||||||
|
// cy.get('#multiSelectList').should('be.visible').first().click();
|
||||||
|
cy.get('#multiSelectList').should('exist').children().first().click();
|
||||||
|
|
||||||
|
// * Check you get a warning when adding a non team member
|
||||||
|
cy.findByTestId('teamWarningBanner').should('contain', '2 users were not selected because they are not a part of this team');
|
||||||
|
|
||||||
|
// * Check the correct username is appearing in the invite to team portion
|
||||||
|
cy.findByTestId('teamWarningBanner').should('contain', `@${newUser.username}`);
|
||||||
|
|
||||||
|
// * Check the guest username is in the warning message and won't be added to the team
|
||||||
|
cy.findByTestId('teamWarningBanner').should('contain', `@${guest.username} is a guest user`);
|
||||||
|
|
||||||
|
// # Click the button "Add" to add user to a channel
|
||||||
|
cy.uiGetButton('Cancel').click();
|
||||||
|
|
||||||
|
// # Wait for the modal to disappear
|
||||||
|
cy.get('#addUsersToChannelModal').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('User doesn\'t have permission to add user to team', () => {
|
||||||
|
cy.apiAdminLogin();
|
||||||
|
|
||||||
|
// # Create a new user
|
||||||
|
cy.apiCreateUser().then(({user: newUser}) => {
|
||||||
|
const id = getRandomId();
|
||||||
|
|
||||||
|
// # Create a custom user group
|
||||||
|
cy.apiCreateCustomUserGroup(`newgroup${id}`, `newgroup${id}`, [newUser.id]).then(() => {
|
||||||
|
// # Create a new channel
|
||||||
|
cy.apiCreateChannel(testTeam.id, 'new-group-channel', 'Channel').then(({channel}) => {
|
||||||
|
cy.apiLogin(firstUser);
|
||||||
|
|
||||||
|
// # Visit a channel
|
||||||
|
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
|
||||||
|
|
||||||
|
// # Open channel menu and click 'Add Members'
|
||||||
|
cy.uiOpenChannelMenu('Add Members');
|
||||||
|
|
||||||
|
// * Assert that modal appears
|
||||||
|
cy.get('#addUsersToChannelModal').should('be.visible');
|
||||||
|
|
||||||
|
// # Type 'group' into the input box
|
||||||
|
cy.get('#selectItems input').typeWithForce(`newgroup${id}`);
|
||||||
|
|
||||||
|
// # Click the first row for a number of times
|
||||||
|
// cy.get('#multiSelectList').should('be.visible').first().click();
|
||||||
|
cy.get('#multiSelectList').should('exist').children().first().click();
|
||||||
|
|
||||||
|
// * Check you get a warning when adding a non team member
|
||||||
|
cy.findByTestId('teamWarningBanner').should('contain', '1 user was not selected because they are not a part of this team');
|
||||||
|
|
||||||
|
// * Check the correct username is appearing in the team invite banner
|
||||||
|
cy.findByTestId('teamWarningBanner').should('contain', `@${newUser.username}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function verifyMentionedUserAndProfilePopover(postId: string) {
|
function verifyMentionedUserAndProfilePopover(postId: string) {
|
||||||
@@ -169,7 +366,7 @@ function verifyMentionedUserAndProfilePopover(postId: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNumberOfUsersToChannel(num = 1) {
|
function addNumberOfUsersToChannel(num = 1, allowExisting = false) {
|
||||||
// # Open channel menu and click 'Add Members'
|
// # Open channel menu and click 'Add Members'
|
||||||
cy.uiOpenChannelMenu('Add Members');
|
cy.uiOpenChannelMenu('Add Members');
|
||||||
cy.get('#addUsersToChannelModal').should('be.visible');
|
cy.get('#addUsersToChannelModal').should('be.visible');
|
||||||
@@ -177,8 +374,14 @@ function addNumberOfUsersToChannel(num = 1) {
|
|||||||
// * Assert that modal appears
|
// * Assert that modal appears
|
||||||
// # Click the first row for a number of times
|
// # Click the first row for a number of times
|
||||||
Cypress._.times(num, () => {
|
Cypress._.times(num, () => {
|
||||||
cy.get('#selectItems input').typeWithForce('u');
|
cy.get('#selectItems input').typeWithForce('user');
|
||||||
cy.get('#multiSelectList').should('be.visible').first().click();
|
|
||||||
|
// cy.get('#multiSelectList').should('be.visible').first().click();
|
||||||
|
if (allowExisting) {
|
||||||
|
cy.get('#multiSelectList').should('exist').children().first().click();
|
||||||
|
} else {
|
||||||
|
cy.get('#multiSelectList').should('exist').children().not(':contains("Already in channel")').first().click();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// # Click the button "Add" to add user to a channel
|
// # Click the button "Add" to add user to a channel
|
||||||
|
|||||||
@@ -155,11 +155,11 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
|
|||||||
cy.findByRole('heading', {name: modalName});
|
cy.findByRole('heading', {name: modalName});
|
||||||
|
|
||||||
// * Verify the accessibility support in search input
|
// * Verify the accessibility support in search input
|
||||||
cy.findByRole('textbox', {name: 'Search for people'}).
|
cy.findByRole('textbox', {name: 'Search for people or groups'}).
|
||||||
should('have.attr', 'aria-autocomplete', 'list');
|
should('have.attr', 'aria-autocomplete', 'list');
|
||||||
|
|
||||||
// # Search for a text and then check up and down arrow
|
// # Search for a text and then check up and down arrow
|
||||||
cy.findByRole('textbox', {name: 'Search for people'}).
|
cy.findByRole('textbox', {name: 'Search for people or groups'}).
|
||||||
typeWithForce('u').
|
typeWithForce('u').
|
||||||
wait(TIMEOUTS.HALF_SEC).
|
wait(TIMEOUTS.HALF_SEC).
|
||||||
typeWithForce('{downarrow}{downarrow}{downarrow}{uparrow}');
|
typeWithForce('{downarrow}{downarrow}{downarrow}{uparrow}');
|
||||||
@@ -184,7 +184,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// # Search for an invalid text and check if reader can read no results
|
// # Search for an invalid text and check if reader can read no results
|
||||||
cy.findByRole('textbox', {name: 'Search for people'}).
|
cy.findByRole('textbox', {name: 'Search for people or groups'}).
|
||||||
typeWithForce('somethingwhichdoesnotexist').
|
typeWithForce('somethingwhichdoesnotexist').
|
||||||
wait(TIMEOUTS.HALF_SEC);
|
wait(TIMEOUTS.HALF_SEC);
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ describe('Channel members test', () => {
|
|||||||
cy.get('#addChannelMembers').click();
|
cy.get('#addChannelMembers').click();
|
||||||
|
|
||||||
// # Enter user1 and user2 emails
|
// # Enter user1 and user2 emails
|
||||||
cy.findByRole('textbox', {name: 'Search for people'}).typeWithForce(`${user1.email}{enter}${user2.email}{enter}`);
|
cy.findByRole('textbox', {name: 'Search for people or groups'}).typeWithForce(`${user1.email}{enter}${user2.email}{enter}`);
|
||||||
|
|
||||||
// # Confirm add the users
|
// # Confirm add the users
|
||||||
cy.get('#addUsersToChannelModal #saveItems').click();
|
cy.get('#addUsersToChannelModal #saveItems').click();
|
||||||
|
|||||||
45
e2e-tests/cypress/tests/support/api/group.ts
Обычный файл
45
e2e-tests/cypress/tests/support/api/group.ts
Обычный файл
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
// *****************************************************************************
|
||||||
|
// Groups
|
||||||
|
// https://api.mattermost.com/#tag/groups
|
||||||
|
// *****************************************************************************
|
||||||
|
|
||||||
|
import {ChainableT} from '../../types';
|
||||||
|
|
||||||
|
function apiCreateCustomUserGroup(displayName: string, name: string, userIds: string[]): ChainableT<Cypress.Group> {
|
||||||
|
return cy.request({
|
||||||
|
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||||
|
url: '/api/v4/groups',
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
display_name: displayName,
|
||||||
|
name,
|
||||||
|
source: 'custom',
|
||||||
|
allow_reference: true,
|
||||||
|
user_ids: userIds,
|
||||||
|
},
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.equal(201);
|
||||||
|
return cy.wrap(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add('apiCreateCustomUserGroup', apiCreateCustomUserGroup);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create custom user group
|
||||||
|
* @param {string} displayName - the display name of the group
|
||||||
|
* @param {string} name - the @ mentionable name of the group
|
||||||
|
* @param {string[]} userIds - users to add to the group
|
||||||
|
*/
|
||||||
|
apiCreateCustomUserGroup: typeof apiCreateCustomUserGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import './cloud';
|
|||||||
import './cluster';
|
import './cluster';
|
||||||
import './common';
|
import './common';
|
||||||
import './data_retention';
|
import './data_retention';
|
||||||
|
import './group';
|
||||||
import './keycloak';
|
import './keycloak';
|
||||||
import './ldap';
|
import './ldap';
|
||||||
import './playbooks';
|
import './playbooks';
|
||||||
|
|||||||
1
e2e-tests/cypress/tests/support/index.d.ts
поставляемый
1
e2e-tests/cypress/tests/support/index.d.ts
поставляемый
@@ -33,6 +33,7 @@ declare namespace Cypress {
|
|||||||
type UserCustomStatus = import('@mattermost/types/users').UserCustomStatus;
|
type UserCustomStatus = import('@mattermost/types/users').UserCustomStatus;
|
||||||
type UserAccessToken = import('@mattermost/types/users').UserAccessToken;
|
type UserAccessToken = import('@mattermost/types/users').UserAccessToken;
|
||||||
type DeepPartial = import('@mattermost/types/utilities').DeepPartial;
|
type DeepPartial = import('@mattermost/types/utilities').DeepPartial;
|
||||||
|
type Group = import('@mattermost/types/groups').Group;
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
tab: (options?: {shift?: boolean}) => Chainable<JQuery>;
|
tab: (options?: {shift?: boolean}) => Chainable<JQuery>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ func getGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
group, appErr := c.App.GetGroup(c.Params.GroupId, &model.GetGroupOpts{
|
group, appErr := c.App.GetGroup(c.Params.GroupId, &model.GetGroupOpts{
|
||||||
IncludeMemberCount: c.Params.IncludeMemberCount,
|
IncludeMemberCount: c.Params.IncludeMemberCount,
|
||||||
|
IncludeMemberIDs: c.Params.IncludeMemberIDs,
|
||||||
}, restrictions)
|
}, restrictions)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
c.Err = appErr
|
c.Err = appErr
|
||||||
@@ -998,6 +999,7 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
Source: source,
|
Source: source,
|
||||||
FilterHasMember: c.Params.FilterHasMember,
|
FilterHasMember: c.Params.FilterHasMember,
|
||||||
IncludeTimezones: includeTimezones,
|
IncludeTimezones: includeTimezones,
|
||||||
|
IncludeMemberIDs: c.Params.IncludeMemberIDs,
|
||||||
IncludeArchived: includeArchived,
|
IncludeArchived: includeArchived,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ func (a *App) GetGroup(id string, opts *model.GetGroupOpts, viewRestrictions *mo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts != nil && opts.IncludeMemberIDs {
|
||||||
|
users, err := a.Srv().Store().Group().GetMemberUsers(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("GetGroup", "app.member_count", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
group.MemberIDs = append(group.MemberIDs, user.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if opts != nil && opts.IncludeMemberCount {
|
if opts != nil && opts.IncludeMemberCount {
|
||||||
memberCount, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(id, viewRestrictions)
|
memberCount, err := a.Srv().Store().Group().GetMemberCountWithRestrictions(id, viewRestrictions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -636,6 +647,19 @@ func (a *App) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestr
|
|||||||
return nil, model.NewAppError("GetGroups", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return nil, model.NewAppError("GetGroups", "app.select_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.IncludeMemberIDs {
|
||||||
|
for _, group := range groups {
|
||||||
|
users, err := a.Srv().Store().Group().GetMemberUsers(group.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("GetGroup", "app.member_count", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
group.MemberIDs = append(group.MemberIDs, user.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return groups, nil
|
return groups, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ type Params struct {
|
|||||||
NotAssociatedToChannel string
|
NotAssociatedToChannel string
|
||||||
Paginate *bool
|
Paginate *bool
|
||||||
IncludeMemberCount bool
|
IncludeMemberCount bool
|
||||||
|
IncludeMemberIDs bool
|
||||||
NotAssociatedToGroup string
|
NotAssociatedToGroup string
|
||||||
ExcludeDefaultChannels bool
|
ExcludeDefaultChannels bool
|
||||||
LimitAfter int
|
LimitAfter int
|
||||||
@@ -218,6 +219,7 @@ func ParamsFromRequest(r *http.Request) *Params {
|
|||||||
}
|
}
|
||||||
|
|
||||||
params.IncludeMemberCount, _ = strconv.ParseBool(query.Get("include_member_count"))
|
params.IncludeMemberCount, _ = strconv.ParseBool(query.Get("include_member_count"))
|
||||||
|
params.IncludeMemberIDs, _ = strconv.ParseBool(query.Get("include_member_ids"))
|
||||||
params.NotAssociatedToGroup = query.Get("not_associated_to_group")
|
params.NotAssociatedToGroup = query.Get("not_associated_to_group")
|
||||||
params.ExcludeDefaultChannels, _ = strconv.ParseBool(query.Get("exclude_default_channels"))
|
params.ExcludeDefaultChannels, _ = strconv.ParseBool(query.Get("exclude_default_channels"))
|
||||||
params.GroupIDs = query.Get("group_ids")
|
params.GroupIDs = query.Get("group_ids")
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type Group struct {
|
|||||||
AllowReference bool `json:"allow_reference"`
|
AllowReference bool `json:"allow_reference"`
|
||||||
ChannelMemberCount *int `db:"-" json:"channel_member_count,omitempty"`
|
ChannelMemberCount *int `db:"-" json:"channel_member_count,omitempty"`
|
||||||
ChannelMemberTimezonesCount *int `db:"-" json:"channel_member_timezones_count,omitempty"`
|
ChannelMemberTimezonesCount *int `db:"-" json:"channel_member_timezones_count,omitempty"`
|
||||||
|
MemberIDs []string `db:"-" json:"member_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (group *Group) Auditable() map[string]interface{} {
|
func (group *Group) Auditable() map[string]interface{} {
|
||||||
@@ -133,6 +134,7 @@ type GroupSearchOpts struct {
|
|||||||
|
|
||||||
IncludeChannelMemberCount string
|
IncludeChannelMemberCount string
|
||||||
IncludeTimezones bool
|
IncludeTimezones bool
|
||||||
|
IncludeMemberIDs bool
|
||||||
|
|
||||||
// Include archived groups
|
// Include archived groups
|
||||||
IncludeArchived bool
|
IncludeArchived bool
|
||||||
@@ -143,6 +145,7 @@ type GroupSearchOpts struct {
|
|||||||
|
|
||||||
type GetGroupOpts struct {
|
type GetGroupOpts struct {
|
||||||
IncludeMemberCount bool
|
IncludeMemberCount bool
|
||||||
|
IncludeMemberIDs bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type PageOpts struct {
|
type PageOpts struct {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {searchAssociatedGroupsForReferenceLocal} from 'mattermost-redux/selector
|
|||||||
import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
|
|
||||||
export function searchAssociatedGroupsForReference(prefix, teamId, channelId) {
|
export function searchAssociatedGroupsForReference(prefix, teamId, channelId, opts = {}) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
if (!haveIChannelPermission(state,
|
if (!haveIChannelPermission(state,
|
||||||
@@ -23,7 +23,7 @@ export function searchAssociatedGroupsForReference(prefix, teamId, channelId) {
|
|||||||
const isTimezoneEnabled = config.ExperimentalTimezone === 'true';
|
const isTimezoneEnabled = config.ExperimentalTimezone === 'true';
|
||||||
|
|
||||||
if (isCustomGroupsEnabled(state)) {
|
if (isCustomGroupsEnabled(state)) {
|
||||||
await dispatch(searchGroups({
|
const params = {
|
||||||
q: prefix,
|
q: prefix,
|
||||||
filter_allow_reference: true,
|
filter_allow_reference: true,
|
||||||
page: 0,
|
page: 0,
|
||||||
@@ -31,7 +31,10 @@ export function searchAssociatedGroupsForReference(prefix, teamId, channelId) {
|
|||||||
include_member_count: true,
|
include_member_count: true,
|
||||||
include_channel_member_count: channelId,
|
include_channel_member_count: channelId,
|
||||||
include_timezones: isTimezoneEnabled,
|
include_timezones: isTimezoneEnabled,
|
||||||
}));
|
...opts,
|
||||||
|
};
|
||||||
|
|
||||||
|
await dispatch(searchGroups(params));
|
||||||
}
|
}
|
||||||
return {data: searchAssociatedGroupsForReferenceLocal(state, prefix, teamId, channelId)};
|
return {data: searchAssociatedGroupsForReferenceLocal(state, prefix, teamId, channelId)};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,19 @@
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&__message {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footerMessage {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message,
|
||||||
|
&__footerMessage {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.AlertBanner__icon {
|
.AlertBanner__icon {
|
||||||
@@ -68,11 +81,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.AlertBanner__message {
|
|
||||||
margin-top: 8px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AlertBanner__closeButton {
|
.AlertBanner__closeButton {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type AlertBannerProps = {
|
|||||||
hideIcon?: boolean;
|
hideIcon?: boolean;
|
||||||
actionButtonLeft?: React.ReactNode;
|
actionButtonLeft?: React.ReactNode;
|
||||||
actionButtonRight?: React.ReactNode;
|
actionButtonRight?: React.ReactNode;
|
||||||
|
footerMessage?: React.ReactNode;
|
||||||
closeBtnTooltip?: React.ReactNode;
|
closeBtnTooltip?: React.ReactNode;
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
variant?: 'sys' | 'app';
|
variant?: 'sys' | 'app';
|
||||||
@@ -47,6 +48,7 @@ const AlertBanner = ({
|
|||||||
actionButtonLeft,
|
actionButtonLeft,
|
||||||
actionButtonRight,
|
actionButtonRight,
|
||||||
closeBtnTooltip,
|
closeBtnTooltip,
|
||||||
|
footerMessage,
|
||||||
hideIcon,
|
hideIcon,
|
||||||
children,
|
children,
|
||||||
}: AlertBannerProps) => {
|
}: AlertBannerProps) => {
|
||||||
@@ -105,6 +107,13 @@ const AlertBanner = ({
|
|||||||
{actionButtonRight}
|
{actionButtonRight}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{
|
||||||
|
footerMessage && (
|
||||||
|
<div className='AlertBanner__footerMessage'>
|
||||||
|
{footerMessage}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{onDismiss && (
|
{onDismiss && (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {openDirectChannelToUserId} from 'actions/channel_actions';
|
|||||||
import {closeModal} from 'actions/views/modals';
|
import {closeModal} from 'actions/views/modals';
|
||||||
import {isModalOpen} from 'selectors/views/modals';
|
import {isModalOpen} from 'selectors/views/modals';
|
||||||
|
|
||||||
import {ListItemType} from 'components/channel_members_rhs/channel_members_rhs';
|
|
||||||
import MemberList from 'components/channel_members_rhs/member_list';
|
import MemberList from 'components/channel_members_rhs/member_list';
|
||||||
|
|
||||||
import {ModalIdentifiers} from 'utils/constants';
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
@@ -44,6 +43,12 @@ export interface ChannelMember {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ListItemType {
|
||||||
|
Member = 'member',
|
||||||
|
FirstSeparator = 'first-separator',
|
||||||
|
Separator = 'separator',
|
||||||
|
}
|
||||||
|
|
||||||
export interface ListItem {
|
export interface ListItem {
|
||||||
type: ListItemType;
|
type: ListItemType;
|
||||||
data: ChannelMember | JSX.Element;
|
data: ChannelMember | JSX.Element;
|
||||||
|
|||||||
@@ -65,9 +65,6 @@ exports[`components/channel_invite_modal should match snapshot for channel_invit
|
|||||||
ariaLabelRenderer={[Function]}
|
ariaLabelRenderer={[Function]}
|
||||||
backButtonClass="btn-cancel tertiary-button"
|
backButtonClass="btn-cancel tertiary-button"
|
||||||
backButtonClick={[Function]}
|
backButtonClick={[Function]}
|
||||||
backButtonText="Cancel"
|
|
||||||
buttonSubmitLoadingText="Adding..."
|
|
||||||
buttonSubmitText="Add"
|
|
||||||
customNoOptionsMessage={null}
|
customNoOptionsMessage={null}
|
||||||
focusOnLoad={true}
|
focusOnLoad={true}
|
||||||
handleAdd={[Function]}
|
handleAdd={[Function]}
|
||||||
@@ -79,24 +76,8 @@ exports[`components/channel_invite_modal should match snapshot for channel_invit
|
|||||||
key="addUsersToChannelKey"
|
key="addUsersToChannelKey"
|
||||||
loading={true}
|
loading={true}
|
||||||
optionRenderer={[Function]}
|
optionRenderer={[Function]}
|
||||||
options={
|
options={Array []}
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"delete_at": 0,
|
|
||||||
"id": "user-1",
|
|
||||||
"label": "user-1",
|
|
||||||
"value": "user-1",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"delete_at": 0,
|
|
||||||
"id": "user-2",
|
|
||||||
"label": "user-2",
|
|
||||||
"value": "user-2",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
perPage={50}
|
perPage={50}
|
||||||
placeholderText="Search for people"
|
|
||||||
saveButtonPosition="bottom"
|
saveButtonPosition="bottom"
|
||||||
saving={false}
|
saving={false}
|
||||||
savingEnabled={true}
|
savingEnabled={true}
|
||||||
@@ -108,6 +89,11 @@ exports[`components/channel_invite_modal should match snapshot for channel_invit
|
|||||||
valueWithImage={true}
|
valueWithImage={true}
|
||||||
values={Array []}
|
values={Array []}
|
||||||
/>
|
/>
|
||||||
|
<Memo(TeamWarningBanner)
|
||||||
|
guests={Array []}
|
||||||
|
teamId="eatxocwc3bg9ffo9xyybnj4omr"
|
||||||
|
users={Array []}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -178,9 +164,6 @@ exports[`components/channel_invite_modal should match snapshot for channel_invit
|
|||||||
ariaLabelRenderer={[Function]}
|
ariaLabelRenderer={[Function]}
|
||||||
backButtonClass="btn-cancel tertiary-button"
|
backButtonClass="btn-cancel tertiary-button"
|
||||||
backButtonClick={[Function]}
|
backButtonClick={[Function]}
|
||||||
backButtonText="Cancel"
|
|
||||||
buttonSubmitLoadingText="Adding..."
|
|
||||||
buttonSubmitText="Add"
|
|
||||||
customNoOptionsMessage={null}
|
customNoOptionsMessage={null}
|
||||||
focusOnLoad={true}
|
focusOnLoad={true}
|
||||||
handleAdd={[Function]}
|
handleAdd={[Function]}
|
||||||
@@ -192,24 +175,8 @@ exports[`components/channel_invite_modal should match snapshot for channel_invit
|
|||||||
key="addUsersToChannelKey"
|
key="addUsersToChannelKey"
|
||||||
loading={true}
|
loading={true}
|
||||||
optionRenderer={[Function]}
|
optionRenderer={[Function]}
|
||||||
options={
|
options={Array []}
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"delete_at": 0,
|
|
||||||
"id": "user-1",
|
|
||||||
"label": "user-1",
|
|
||||||
"value": "user-1",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"delete_at": 0,
|
|
||||||
"id": "user-2",
|
|
||||||
"label": "user-2",
|
|
||||||
"value": "user-2",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
perPage={50}
|
perPage={50}
|
||||||
placeholderText="Search for people"
|
|
||||||
saveButtonPosition="bottom"
|
saveButtonPosition="bottom"
|
||||||
saving={false}
|
saving={false}
|
||||||
savingEnabled={true}
|
savingEnabled={true}
|
||||||
@@ -221,52 +188,36 @@ exports[`components/channel_invite_modal should match snapshot for channel_invit
|
|||||||
valueWithImage={true}
|
valueWithImage={true}
|
||||||
values={Array []}
|
values={Array []}
|
||||||
/>
|
/>
|
||||||
|
<Memo(TeamWarningBanner)
|
||||||
|
guests={Array []}
|
||||||
|
teamId="eatxocwc3bg9ffo9xyybnj4omr"
|
||||||
|
users={Array []}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/channel_invite_modal should match snapshot for channel_invite_modal with userStatuses 1`] = `
|
exports[`components/channel_invite_modal should match snapshot for channel_invite_modal with userStatuses 1`] = `
|
||||||
<div
|
<Memo(GroupOption)
|
||||||
className="more-modal__row clickable more-modal__row--selected"
|
addUserProfile={[MockFunction]}
|
||||||
onClick={[Function]}
|
group={
|
||||||
onMouseMove={[Function]}
|
Object {
|
||||||
>
|
"delete_at": 0,
|
||||||
<ProfilePicture
|
"id": "user-1",
|
||||||
hasMention={false}
|
"label": "user-1",
|
||||||
isEmoji={false}
|
"value": "user-1",
|
||||||
popoverPlacement="right"
|
}
|
||||||
size="md"
|
}
|
||||||
src="/api/v4/users/user-1/image"
|
isSelected={true}
|
||||||
status="online"
|
onMouseMove={[MockFunction]}
|
||||||
wrapperClass=""
|
rowSelected="more-modal__row--selected"
|
||||||
/>
|
selectedItemRef={
|
||||||
<div
|
Object {
|
||||||
className="more-modal__details"
|
"current": null,
|
||||||
>
|
}
|
||||||
<div
|
}
|
||||||
className="more-modal__name"
|
/>
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="d-flex"
|
|
||||||
>
|
|
||||||
<span />
|
|
||||||
<span
|
|
||||||
className="ml-2 light flex-auto"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="more-modal__actions"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="more-modal__actions--round"
|
|
||||||
>
|
|
||||||
<AddIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/channel_invite_modal should match snapshot with exclude and include users 1`] = `
|
exports[`components/channel_invite_modal should match snapshot with exclude and include users 1`] = `
|
||||||
@@ -334,9 +285,6 @@ exports[`components/channel_invite_modal should match snapshot with exclude and
|
|||||||
ariaLabelRenderer={[Function]}
|
ariaLabelRenderer={[Function]}
|
||||||
backButtonClass="btn-cancel tertiary-button"
|
backButtonClass="btn-cancel tertiary-button"
|
||||||
backButtonClick={[Function]}
|
backButtonClick={[Function]}
|
||||||
backButtonText="Cancel"
|
|
||||||
buttonSubmitLoadingText="Adding..."
|
|
||||||
buttonSubmitText="Add"
|
|
||||||
customNoOptionsMessage={null}
|
customNoOptionsMessage={null}
|
||||||
focusOnLoad={true}
|
focusOnLoad={true}
|
||||||
handleAdd={[Function]}
|
handleAdd={[Function]}
|
||||||
@@ -348,24 +296,8 @@ exports[`components/channel_invite_modal should match snapshot with exclude and
|
|||||||
key="addUsersToChannelKey"
|
key="addUsersToChannelKey"
|
||||||
loading={true}
|
loading={true}
|
||||||
optionRenderer={[Function]}
|
optionRenderer={[Function]}
|
||||||
options={
|
options={Array []}
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"delete_at": 0,
|
|
||||||
"id": "user-2",
|
|
||||||
"label": "user-2",
|
|
||||||
"value": "user-2",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"delete_at": 0,
|
|
||||||
"id": "user-3",
|
|
||||||
"label": "user-3",
|
|
||||||
"value": "user-3",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
perPage={50}
|
perPage={50}
|
||||||
placeholderText="Search for people"
|
|
||||||
saveButtonPosition="bottom"
|
saveButtonPosition="bottom"
|
||||||
saving={false}
|
saving={false}
|
||||||
savingEnabled={true}
|
savingEnabled={true}
|
||||||
@@ -377,6 +309,11 @@ exports[`components/channel_invite_modal should match snapshot with exclude and
|
|||||||
valueWithImage={true}
|
valueWithImage={true}
|
||||||
values={Array []}
|
values={Array []}
|
||||||
/>
|
/>
|
||||||
|
<Memo(TeamWarningBanner)
|
||||||
|
guests={Array []}
|
||||||
|
teamId="eatxocwc3bg9ffo9xyybnj4omr"
|
||||||
|
users={Array []}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ import type {Value} from 'components/multiselect/multiselect';
|
|||||||
|
|
||||||
type UserProfileValue = Value & UserProfile;
|
type UserProfileValue = Value & UserProfile;
|
||||||
|
|
||||||
|
jest.mock('utils/utils', () => {
|
||||||
|
const original = jest.requireActual('utils/utils');
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
localizeMessage: jest.fn(),
|
||||||
|
sortUsersAndGroups: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('components/channel_invite_modal', () => {
|
describe('components/channel_invite_modal', () => {
|
||||||
const users = [{
|
const users = [{
|
||||||
id: 'user-1',
|
id: 'user-1',
|
||||||
@@ -55,8 +64,11 @@ describe('components/channel_invite_modal', () => {
|
|||||||
profilesInCurrentChannel: [],
|
profilesInCurrentChannel: [],
|
||||||
profilesNotInCurrentTeam: [],
|
profilesNotInCurrentTeam: [],
|
||||||
profilesFromRecentDMs: [],
|
profilesFromRecentDMs: [],
|
||||||
|
membersInTeam: {},
|
||||||
|
groups: [],
|
||||||
userStatuses: {},
|
userStatuses: {},
|
||||||
teammateNameDisplaySetting: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME,
|
teammateNameDisplaySetting: General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME,
|
||||||
|
isGroupsEnabled: true,
|
||||||
actions: {
|
actions: {
|
||||||
addUsersToChannel: jest.fn().mockImplementation(() => {
|
addUsersToChannel: jest.fn().mockImplementation(() => {
|
||||||
const error = {
|
const error = {
|
||||||
@@ -67,11 +79,13 @@ describe('components/channel_invite_modal', () => {
|
|||||||
}),
|
}),
|
||||||
getProfilesNotInChannel: jest.fn().mockImplementation(() => Promise.resolve()),
|
getProfilesNotInChannel: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
getProfilesInChannel: jest.fn().mockImplementation(() => Promise.resolve()),
|
getProfilesInChannel: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
|
searchAssociatedGroupsForReference: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
getTeamStats: jest.fn(),
|
getTeamStats: jest.fn(),
|
||||||
getUserStatuses: jest.fn().mockImplementation(() => Promise.resolve()),
|
getUserStatuses: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
loadStatusesForProfilesList: jest.fn(),
|
loadStatusesForProfilesList: jest.fn(),
|
||||||
searchProfiles: jest.fn(),
|
searchProfiles: jest.fn(),
|
||||||
closeModal: jest.fn(),
|
closeModal: jest.fn(),
|
||||||
|
getTeamMembersByIds: jest.fn(),
|
||||||
},
|
},
|
||||||
onExited: jest.fn(),
|
onExited: jest.fn(),
|
||||||
};
|
};
|
||||||
@@ -176,7 +190,7 @@ describe('components/channel_invite_modal', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.setState({values: users, show: true});
|
wrapper.setState({selectedUsers: users, show: true});
|
||||||
wrapper.instance().handleSubmit();
|
wrapper.instance().handleSubmit();
|
||||||
expect(wrapper.state('saving')).toEqual(true);
|
expect(wrapper.state('saving')).toEqual(true);
|
||||||
expect(wrapper.instance().props.actions.addUsersToChannel).toHaveBeenCalledTimes(1);
|
expect(wrapper.instance().props.actions.addUsersToChannel).toHaveBeenCalledTimes(1);
|
||||||
@@ -205,7 +219,7 @@ describe('components/channel_invite_modal', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.setState({values: users, show: true});
|
wrapper.setState({selectedUsers: users, show: true});
|
||||||
wrapper.instance().handleSubmit();
|
wrapper.instance().handleSubmit();
|
||||||
expect(wrapper.state('saving')).toEqual(true);
|
expect(wrapper.state('saving')).toEqual(true);
|
||||||
expect(wrapper.instance().props.actions.addUsersToChannel).toHaveBeenCalledTimes(1);
|
expect(wrapper.instance().props.actions.addUsersToChannel).toHaveBeenCalledTimes(1);
|
||||||
@@ -231,7 +245,7 @@ describe('components/channel_invite_modal', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.setState({values: users, show: true});
|
wrapper.setState({selectedUsers: users, show: true});
|
||||||
wrapper.instance().handleSubmit();
|
wrapper.instance().handleSubmit();
|
||||||
expect(onAddCallback).toHaveBeenCalled();
|
expect(onAddCallback).toHaveBeenCalled();
|
||||||
expect(wrapper.instance().props.actions.addUsersToChannel).toHaveBeenCalledTimes(0);
|
expect(wrapper.instance().props.actions.addUsersToChannel).toHaveBeenCalledTimes(0);
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {isEqual} from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Modal} from 'react-bootstrap';
|
import {Modal} from 'react-bootstrap';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
import type {Channel} from '@mattermost/types/channels';
|
||||||
|
import type {Group, GroupSearchParams} from '@mattermost/types/groups';
|
||||||
|
import type {TeamMembership} from '@mattermost/types/teams';
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
import type {RelationOneToOne} from '@mattermost/types/utilities';
|
import type {RelationOneToOne} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
|
import {filterGroupsMatchingTerm} from 'mattermost-redux/utils/group_utils';
|
||||||
import {displayUsername, filterProfilesStartingWithTerm, isGuest} from 'mattermost-redux/utils/user_utils';
|
import {displayUsername, filterProfilesStartingWithTerm, isGuest} from 'mattermost-redux/utils/user_utils';
|
||||||
|
|
||||||
import InvitationModal from 'components/invitation_modal';
|
import InvitationModal from 'components/invitation_modal';
|
||||||
@@ -23,7 +28,10 @@ import BotTag from 'components/widgets/tag/bot_tag';
|
|||||||
import GuestTag from 'components/widgets/tag/guest_tag';
|
import GuestTag from 'components/widgets/tag/guest_tag';
|
||||||
|
|
||||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||||
import {localizeMessage} from 'utils/utils';
|
import {localizeMessage, sortUsersAndGroups} from 'utils/utils';
|
||||||
|
|
||||||
|
import GroupOption from './group_option';
|
||||||
|
import TeamWarningBanner from './team_warning_banner';
|
||||||
|
|
||||||
const USERS_PER_PAGE = 50;
|
const USERS_PER_PAGE = 50;
|
||||||
const USERS_FROM_DMS = 10;
|
const USERS_FROM_DMS = 10;
|
||||||
@@ -31,11 +39,14 @@ const MAX_USERS = 25;
|
|||||||
|
|
||||||
type UserProfileValue = Value & UserProfile;
|
type UserProfileValue = Value & UserProfile;
|
||||||
|
|
||||||
|
type GroupValue = Value & Group;
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
profilesNotInCurrentChannel: UserProfileValue[];
|
profilesNotInCurrentChannel: UserProfile[];
|
||||||
profilesInCurrentChannel: UserProfileValue[];
|
profilesInCurrentChannel: UserProfile[];
|
||||||
profilesNotInCurrentTeam: UserProfileValue[];
|
profilesNotInCurrentTeam: UserProfile[];
|
||||||
profilesFromRecentDMs: UserProfile[];
|
profilesFromRecentDMs: UserProfile[];
|
||||||
|
membersInTeam: RelationOneToOne<UserProfile, TeamMembership>;
|
||||||
userStatuses: RelationOneToOne<UserProfile, string>;
|
userStatuses: RelationOneToOne<UserProfile, string>;
|
||||||
onExited: () => void;
|
onExited: () => void;
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
@@ -52,6 +63,8 @@ export type Props = {
|
|||||||
includeUsers?: Record<string, UserProfileValue>;
|
includeUsers?: Record<string, UserProfileValue>;
|
||||||
canInviteGuests?: boolean;
|
canInviteGuests?: boolean;
|
||||||
emailInvitationsEnabled?: boolean;
|
emailInvitationsEnabled?: boolean;
|
||||||
|
groups: Group[];
|
||||||
|
isGroupsEnabled: boolean;
|
||||||
actions: {
|
actions: {
|
||||||
addUsersToChannel: (channelId: string, userIds: string[]) => Promise<ActionResult>;
|
addUsersToChannel: (channelId: string, userIds: string[]) => Promise<ActionResult>;
|
||||||
getProfilesNotInChannel: (teamId: string, channelId: string, groupConstrained: boolean, page: number, perPage?: number) => Promise<ActionResult>;
|
getProfilesNotInChannel: (teamId: string, channelId: string, groupConstrained: boolean, page: number, perPage?: number) => Promise<ActionResult>;
|
||||||
@@ -60,11 +73,16 @@ export type Props = {
|
|||||||
loadStatusesForProfilesList: (users: UserProfile[]) => void;
|
loadStatusesForProfilesList: (users: UserProfile[]) => void;
|
||||||
searchProfiles: (term: string, options: any) => Promise<ActionResult>;
|
searchProfiles: (term: string, options: any) => Promise<ActionResult>;
|
||||||
closeModal: (modalId: string) => void;
|
closeModal: (modalId: string) => void;
|
||||||
|
searchAssociatedGroupsForReference: (prefix: string, teamId: string, channelId: string | undefined, opts: GroupSearchParams) => Promise<ActionResult>;
|
||||||
|
getTeamMembersByIds: (teamId: string, userIds: string[]) => Promise<ActionResult>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
values: UserProfileValue[];
|
selectedUsers: UserProfileValue[];
|
||||||
|
groupAndUserOptions: Array<UserProfileValue | GroupValue>;
|
||||||
|
usersNotInTeam: UserProfileValue[];
|
||||||
|
guestsNotInTeam: UserProfileValue[];
|
||||||
term: string;
|
term: string;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
@@ -72,6 +90,15 @@ type State = {
|
|||||||
inviteError?: string;
|
inviteError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UsernameSpan = styled.span`
|
||||||
|
fontSize: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserMappingSpan = styled.span`
|
||||||
|
position: 'absolute';
|
||||||
|
right: 20;
|
||||||
|
`;
|
||||||
|
|
||||||
export default class ChannelInviteModal extends React.PureComponent<Props, State> {
|
export default class ChannelInviteModal extends React.PureComponent<Props, State> {
|
||||||
private searchTimeoutId = 0;
|
private searchTimeoutId = 0;
|
||||||
private selectedItemRef = React.createRef<HTMLDivElement>();
|
private selectedItemRef = React.createRef<HTMLDivElement>();
|
||||||
@@ -85,21 +112,70 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
values: [],
|
selectedUsers: [],
|
||||||
|
usersNotInTeam: [],
|
||||||
|
guestsNotInTeam: [],
|
||||||
term: '',
|
term: '',
|
||||||
show: true,
|
show: true,
|
||||||
saving: false,
|
saving: false,
|
||||||
loadingUsers: true,
|
loadingUsers: true,
|
||||||
|
groupAndUserOptions: [],
|
||||||
} as State;
|
} as State;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addValue = (value: UserProfileValue): void => {
|
isUser = (option: UserProfileValue | GroupValue): option is UserProfileValue => {
|
||||||
const values: UserProfileValue[] = Object.assign([], this.state.values);
|
return (option as UserProfile).username !== undefined;
|
||||||
if (values.indexOf(value) === -1) {
|
};
|
||||||
values.push(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({values});
|
private addValue = (value: UserProfileValue | GroupValue): void => {
|
||||||
|
if (this.isUser(value)) {
|
||||||
|
const profile = value;
|
||||||
|
if (!this.props.membersInTeam || !this.props.membersInTeam[profile.id]) {
|
||||||
|
if (isGuest(profile.roles)) {
|
||||||
|
if (this.state.guestsNotInTeam.indexOf(profile) === -1) {
|
||||||
|
this.setState((prevState) => {
|
||||||
|
return {guestsNotInTeam: [...prevState.guestsNotInTeam, profile]};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.state.usersNotInTeam.indexOf(profile) === -1) {
|
||||||
|
this.setState((prevState) => {
|
||||||
|
return {usersNotInTeam: [...prevState.usersNotInTeam, profile]};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.selectedUsers.indexOf(profile) === -1) {
|
||||||
|
this.setState((prevState) => {
|
||||||
|
return {selectedUsers: [...prevState.selectedUsers, profile]};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private removeInvitedUsers = (profiles: UserProfile[]): void => {
|
||||||
|
const usersNotInTeam = this.state.usersNotInTeam.filter((profile) => {
|
||||||
|
const user = profile as UserProfileValue;
|
||||||
|
|
||||||
|
const index = profiles.indexOf(user);
|
||||||
|
if (index === -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.addValue(user);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({usersNotInTeam: [...usersNotInTeam], guestsNotInTeam: []});
|
||||||
|
};
|
||||||
|
|
||||||
|
private removeUsersFromValuesNotInTeam = (profiles: UserProfile[]): void => {
|
||||||
|
const usersNotInTeam = this.state.usersNotInTeam.filter((profile) => {
|
||||||
|
const index = profiles.indexOf(profile);
|
||||||
|
return index === -1;
|
||||||
|
});
|
||||||
|
this.setState({usersNotInTeam: [...usersNotInTeam], guestsNotInTeam: []});
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
@@ -112,6 +188,62 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
this.props.actions.loadStatusesForProfilesList(this.props.profilesInCurrentChannel);
|
this.props.actions.loadStatusesForProfilesList(this.props.profilesInCurrentChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
|
if (prevState.term !== this.state.term) {
|
||||||
|
const values = this.getOptions();
|
||||||
|
const userIds: string[] = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < values.length; index++) {
|
||||||
|
const newValue = values[index];
|
||||||
|
if (this.isUser(newValue)) {
|
||||||
|
userIds.push(newValue.id);
|
||||||
|
} else if (newValue.member_ids) {
|
||||||
|
userIds.push(...newValue.member_ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEqual(values, this.state.groupAndUserOptions)) {
|
||||||
|
if (userIds.length > 0) {
|
||||||
|
this.props.actions.getTeamMembersByIds(this.props.channel.team_id, userIds);
|
||||||
|
}
|
||||||
|
this.setState({groupAndUserOptions: values});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getExcludedUsers = (): Set<string> => {
|
||||||
|
if (this.props.excludeUsers) {
|
||||||
|
return new Set(...this.props.profilesNotInCurrentTeam.map((user) => user.id), Object.values(this.props.excludeUsers).map((user) => user.id));
|
||||||
|
}
|
||||||
|
return new Set(this.props.profilesNotInCurrentTeam.map((user) => user.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Options list prioritizes recent dms for the first 10 users and then the next 15 are a mix of users and groups
|
||||||
|
public getOptions = () => {
|
||||||
|
const excludedAndNotInTeamUserIds = this.getExcludedUsers();
|
||||||
|
|
||||||
|
const filteredDmUsers = filterProfilesStartingWithTerm(this.props.profilesFromRecentDMs, this.state.term);
|
||||||
|
const dmUsers = this.filterOutDeletedAndExcludedAndNotInTeamUsers(filteredDmUsers, excludedAndNotInTeamUserIds).slice(0, USERS_FROM_DMS) as UserProfileValue[];
|
||||||
|
|
||||||
|
let users: UserProfileValue[];
|
||||||
|
const filteredUsers: UserProfile[] = filterProfilesStartingWithTerm(this.props.profilesNotInCurrentChannel.concat(this.props.profilesInCurrentChannel), this.state.term);
|
||||||
|
users = this.filterOutDeletedAndExcludedAndNotInTeamUsers(filteredUsers, excludedAndNotInTeamUserIds);
|
||||||
|
if (this.props.includeUsers) {
|
||||||
|
users = [...users, ...Object.values(this.props.includeUsers)];
|
||||||
|
}
|
||||||
|
const groupsAndUsers = [
|
||||||
|
...filterGroupsMatchingTerm(this.props.groups, this.state.term) as GroupValue[],
|
||||||
|
...users,
|
||||||
|
].sort(sortUsersAndGroups);
|
||||||
|
|
||||||
|
const optionValues = [
|
||||||
|
...dmUsers,
|
||||||
|
...groupsAndUsers,
|
||||||
|
].slice(0, MAX_USERS);
|
||||||
|
|
||||||
|
return Array.from(new Set(optionValues));
|
||||||
|
};
|
||||||
|
|
||||||
public onHide = (): void => {
|
public onHide = (): void => {
|
||||||
this.setState({show: false});
|
this.setState({show: false});
|
||||||
this.props.actions.loadStatusesForProfilesList(this.props.profilesNotInCurrentChannel);
|
this.props.actions.loadStatusesForProfilesList(this.props.profilesNotInCurrentChannel);
|
||||||
@@ -127,8 +259,10 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleDelete = (values: UserProfileValue[]): void => {
|
private handleDelete = (values: Array<UserProfileValue | GroupValue>): void => {
|
||||||
this.setState({values});
|
// Our values for this component are always UserProfileValue
|
||||||
|
const profiles = values as UserProfileValue[];
|
||||||
|
this.setState({selectedUsers: profiles});
|
||||||
};
|
};
|
||||||
|
|
||||||
private setUsersLoadingState = (loadingState: boolean): void => {
|
private setUsersLoadingState = (loadingState: boolean): void => {
|
||||||
@@ -153,13 +287,13 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
public handleSubmit = (): void => {
|
public handleSubmit = (): void => {
|
||||||
const {actions, channel} = this.props;
|
const {actions, channel} = this.props;
|
||||||
|
|
||||||
const userIds = this.state.values.map((v) => v.id);
|
const userIds = this.state.selectedUsers.map((u) => u.id);
|
||||||
if (userIds.length === 0) {
|
if (userIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.skipCommit && this.props.onAddCallback) {
|
if (this.props.skipCommit && this.props.onAddCallback) {
|
||||||
this.props.onAddCallback(this.state.values);
|
this.props.onAddCallback(this.state.selectedUsers);
|
||||||
this.setState({
|
this.setState({
|
||||||
saving: false,
|
saving: false,
|
||||||
inviteError: undefined,
|
inviteError: undefined,
|
||||||
@@ -190,24 +324,6 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
term,
|
term,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (term) {
|
|
||||||
this.setUsersLoadingState(true);
|
|
||||||
this.searchTimeoutId = window.setTimeout(
|
|
||||||
async () => {
|
|
||||||
const options = {
|
|
||||||
team_id: this.props.channel.team_id,
|
|
||||||
not_in_channel_id: this.props.channel.id,
|
|
||||||
group_constrained: this.props.channel.group_constrained,
|
|
||||||
};
|
|
||||||
await this.props.actions.searchProfiles(term, options);
|
|
||||||
this.setUsersLoadingState(false);
|
|
||||||
},
|
|
||||||
Constants.SEARCH_TIMEOUT_MILLISECONDS,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchTimeoutId = window.setTimeout(
|
this.searchTimeoutId = window.setTimeout(
|
||||||
async () => {
|
async () => {
|
||||||
if (!term) {
|
if (!term) {
|
||||||
@@ -219,18 +335,36 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
not_in_channel_id: this.props.channel.id,
|
not_in_channel_id: this.props.channel.id,
|
||||||
group_constrained: this.props.channel.group_constrained,
|
group_constrained: this.props.channel.group_constrained,
|
||||||
};
|
};
|
||||||
await this.props.actions.searchProfiles(term, options);
|
|
||||||
|
const opts = {
|
||||||
|
q: term,
|
||||||
|
filter_allow_reference: true,
|
||||||
|
page: 0,
|
||||||
|
per_page: 100,
|
||||||
|
include_member_count: true,
|
||||||
|
include_member_ids: true,
|
||||||
|
};
|
||||||
|
const promises = [
|
||||||
|
this.props.actions.searchProfiles(term, options),
|
||||||
|
];
|
||||||
|
if (this.props.isGroupsEnabled) {
|
||||||
|
promises.push(this.props.actions.searchAssociatedGroupsForReference(term, this.props.channel.team_id, this.props.channel.id, opts));
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
this.setUsersLoadingState(false);
|
this.setUsersLoadingState(false);
|
||||||
},
|
},
|
||||||
Constants.SEARCH_TIMEOUT_MILLISECONDS,
|
Constants.SEARCH_TIMEOUT_MILLISECONDS,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderAriaLabel = (option: UserProfileValue): string => {
|
private renderAriaLabel = (option: UserProfileValue | GroupValue): string => {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return option.username;
|
if (this.isUser(option)) {
|
||||||
|
return option.username;
|
||||||
|
}
|
||||||
|
return option.name;
|
||||||
};
|
};
|
||||||
|
|
||||||
private filterOutDeletedAndExcludedAndNotInTeamUsers = (users: UserProfile[], excludeUserIds: Set<string>): UserProfileValue[] => {
|
private filterOutDeletedAndExcludedAndNotInTeamUsers = (users: UserProfile[], excludeUserIds: Set<string>): UserProfileValue[] => {
|
||||||
@@ -239,63 +373,75 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
}) as UserProfileValue[];
|
}) as UserProfileValue[];
|
||||||
};
|
};
|
||||||
|
|
||||||
renderOption = (option: UserProfileValue, isSelected: boolean, onAdd: (user: UserProfileValue) => void, onMouseMove: (user: UserProfileValue) => void) => {
|
renderOption = (option: UserProfileValue | GroupValue, isSelected: boolean, onAdd: (option: UserProfileValue | GroupValue) => void, onMouseMove: (option: UserProfileValue | GroupValue) => void) => {
|
||||||
let rowSelected = '';
|
let rowSelected = '';
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
rowSelected = 'more-modal__row--selected';
|
rowSelected = 'more-modal__row--selected';
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfilesInGroup = this.props.profilesInCurrentChannel.map((user) => user.id);
|
if (this.isUser(option)) {
|
||||||
|
const ProfilesInGroup = this.props.profilesInCurrentChannel.map((user) => user.id);
|
||||||
|
|
||||||
const userMapping: Record<string, string> = {};
|
const userMapping: Record<string, string> = {};
|
||||||
|
for (let i = 0; i < ProfilesInGroup.length; i++) {
|
||||||
for (let i = 0; i < ProfilesInGroup.length; i++) {
|
userMapping[ProfilesInGroup[i]] = 'Already in channel';
|
||||||
userMapping[ProfilesInGroup[i]] = 'Already in channel';
|
}
|
||||||
|
const displayName = displayUsername(option, this.props.teammateNameDisplaySetting);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.id}
|
||||||
|
ref={isSelected ? this.selectedItemRef : option.id}
|
||||||
|
className={'more-modal__row clickable ' + rowSelected}
|
||||||
|
onClick={() => onAdd(option)}
|
||||||
|
onMouseMove={() => onMouseMove(option)}
|
||||||
|
>
|
||||||
|
<ProfilePicture
|
||||||
|
src={Client4.getProfilePictureUrl(option.id, option.last_picture_update)}
|
||||||
|
status={this.props.userStatuses[option.id]}
|
||||||
|
size='md'
|
||||||
|
username={option.username}
|
||||||
|
/>
|
||||||
|
<div className='more-modal__details'>
|
||||||
|
<div className='more-modal__name'>
|
||||||
|
<span>
|
||||||
|
{displayName}
|
||||||
|
{option.is_bot && <BotTag/>}
|
||||||
|
{isGuest(option.roles) && <GuestTag className='popoverlist'/>}
|
||||||
|
{displayName === option.username ?
|
||||||
|
null :
|
||||||
|
<UsernameSpan
|
||||||
|
className='ml-2 light'
|
||||||
|
>
|
||||||
|
{'@'}{option.username}
|
||||||
|
</UsernameSpan>
|
||||||
|
}
|
||||||
|
<UserMappingSpan
|
||||||
|
className='light'
|
||||||
|
>
|
||||||
|
{userMapping[option.id]}
|
||||||
|
</UserMappingSpan>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='more-modal__actions'>
|
||||||
|
<div className='more-modal__actions--round'>
|
||||||
|
<AddIcon/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = displayUsername(option, this.props.teammateNameDisplaySetting);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<GroupOption
|
||||||
|
group={option}
|
||||||
key={option.id}
|
key={option.id}
|
||||||
ref={isSelected ? this.selectedItemRef : option.id}
|
addUserProfile={onAdd}
|
||||||
className={'more-modal__row clickable ' + rowSelected}
|
isSelected={isSelected}
|
||||||
onClick={() => onAdd(option)}
|
rowSelected={rowSelected}
|
||||||
onMouseMove={() => onMouseMove(option)}
|
onMouseMove={onMouseMove}
|
||||||
>
|
selectedItemRef={this.selectedItemRef}
|
||||||
<ProfilePicture
|
/>
|
||||||
src={Client4.getProfilePictureUrl(option.id, option.last_picture_update)}
|
|
||||||
status={this.props.userStatuses[option.id]}
|
|
||||||
size='md'
|
|
||||||
username={option.username}
|
|
||||||
/>
|
|
||||||
<div className='more-modal__details'>
|
|
||||||
<div className='more-modal__name'>
|
|
||||||
<span className='d-flex'>
|
|
||||||
<span>{displayName}</span>
|
|
||||||
{option.is_bot && <BotTag/>}
|
|
||||||
{isGuest(option.roles) && <GuestTag className='popoverlist'/>}
|
|
||||||
{displayName === option.username ? null : (
|
|
||||||
<span
|
|
||||||
className='ml-2 light flex-auto'
|
|
||||||
>
|
|
||||||
{'@'}{option.username}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className='ml-2 light flex-auto'
|
|
||||||
>
|
|
||||||
{userMapping[option.id]}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='more-modal__actions'>
|
|
||||||
<div className='more-modal__actions--round'>
|
|
||||||
<AddIcon/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -319,31 +465,6 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
|
|
||||||
const buttonSubmitText = localizeMessage('multiselect.add', 'Add');
|
const buttonSubmitText = localizeMessage('multiselect.add', 'Add');
|
||||||
const buttonSubmitLoadingText = localizeMessage('multiselect.adding', 'Adding...');
|
const buttonSubmitLoadingText = localizeMessage('multiselect.adding', 'Adding...');
|
||||||
let excludedAndNotInTeamUserIds: Set<string>;
|
|
||||||
if (this.props.excludeUsers) {
|
|
||||||
excludedAndNotInTeamUserIds = new Set(...this.props.profilesNotInCurrentTeam.map((user) => user.id), Object.values(this.props.excludeUsers).map((user) => user.id));
|
|
||||||
} else {
|
|
||||||
excludedAndNotInTeamUserIds = new Set(this.props.profilesNotInCurrentTeam.map((user) => user.id));
|
|
||||||
}
|
|
||||||
let users = this.filterOutDeletedAndExcludedAndNotInTeamUsers(
|
|
||||||
filterProfilesStartingWithTerm(
|
|
||||||
this.props.profilesNotInCurrentChannel.concat(this.props.profilesInCurrentChannel),
|
|
||||||
this.state.term),
|
|
||||||
excludedAndNotInTeamUserIds);
|
|
||||||
if (this.props.includeUsers) {
|
|
||||||
const includeUsers = Object.values(this.props.includeUsers);
|
|
||||||
users = [...users, ...includeUsers];
|
|
||||||
}
|
|
||||||
users = [
|
|
||||||
...this.filterOutDeletedAndExcludedAndNotInTeamUsers(
|
|
||||||
filterProfilesStartingWithTerm(this.props.profilesFromRecentDMs, this.state.term),
|
|
||||||
excludedAndNotInTeamUserIds).
|
|
||||||
slice(0, USERS_FROM_DMS) as UserProfileValue[],
|
|
||||||
...users,
|
|
||||||
].
|
|
||||||
slice(0, MAX_USERS);
|
|
||||||
|
|
||||||
users = Array.from(new Set(users));
|
|
||||||
|
|
||||||
const closeMembersInviteModal = () => {
|
const closeMembersInviteModal = () => {
|
||||||
this.props.actions.closeModal(ModalIdentifiers.CHANNEL_INVITE);
|
this.props.actions.closeModal(ModalIdentifiers.CHANNEL_INVITE);
|
||||||
@@ -387,10 +508,10 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
const content = (
|
const content = (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
key='addUsersToChannelKey'
|
key='addUsersToChannelKey'
|
||||||
options={users}
|
options={this.state.groupAndUserOptions}
|
||||||
optionRenderer={this.renderOption}
|
optionRenderer={this.renderOption}
|
||||||
selectedItemRef={this.selectedItemRef}
|
selectedItemRef={this.selectedItemRef}
|
||||||
values={this.state.values}
|
values={this.state.selectedUsers}
|
||||||
ariaLabelRenderer={this.renderAriaLabel}
|
ariaLabelRenderer={this.renderAriaLabel}
|
||||||
saveButtonPosition={'bottom'}
|
saveButtonPosition={'bottom'}
|
||||||
perPage={USERS_PER_PAGE}
|
perPage={USERS_PER_PAGE}
|
||||||
@@ -404,7 +525,7 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
buttonSubmitLoadingText={buttonSubmitLoadingText}
|
buttonSubmitLoadingText={buttonSubmitLoadingText}
|
||||||
saving={this.state.saving}
|
saving={this.state.saving}
|
||||||
loading={this.state.loadingUsers}
|
loading={this.state.loadingUsers}
|
||||||
placeholderText={localizeMessage('multiselect.placeholder', 'Search for people')}
|
placeholderText={this.props.isGroupsEnabled ? localizeMessage('multiselect.placeholder.peopleOrGroups', 'Search for people or groups') : localizeMessage('multiselect.placeholder', 'Search for people')}
|
||||||
valueWithImage={true}
|
valueWithImage={true}
|
||||||
backButtonText={localizeMessage('multiselect.cancel', 'Cancel')}
|
backButtonText={localizeMessage('multiselect.cancel', 'Cancel')}
|
||||||
backButtonClick={closeMembersInviteModal}
|
backButtonClick={closeMembersInviteModal}
|
||||||
@@ -446,6 +567,11 @@ export default class ChannelInviteModal extends React.PureComponent<Props, State
|
|||||||
{inviteError}
|
{inviteError}
|
||||||
<div className='channel-invite__content'>
|
<div className='channel-invite__content'>
|
||||||
{content}
|
{content}
|
||||||
|
<TeamWarningBanner
|
||||||
|
guests={this.state.guestsNotInTeam}
|
||||||
|
teamId={this.props.channel.team_id}
|
||||||
|
users={this.state.usersNotInTeam}
|
||||||
|
/>
|
||||||
{(this.props.emailInvitationsEnabled && this.props.canInviteGuests) && inviteGuestLink}
|
{(this.props.emailInvitationsEnabled && this.props.canInviteGuests) && inviteGuestLink}
|
||||||
</div>
|
</div>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useEffect} from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {AccountMultipleOutlineIcon, ChevronRightIcon} from '@mattermost/compass-icons/components';
|
||||||
|
import type {Group} from '@mattermost/types/groups';
|
||||||
|
import type {GlobalState} from '@mattermost/types/store';
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {getUser, makeDisplayNameGetter, makeGetProfilesByIdsAndUsernames} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
|
import type {Value} from 'components/multiselect/multiselect';
|
||||||
|
import SimpleTooltip from 'components/widgets/simple_tooltip';
|
||||||
|
|
||||||
|
import Constants from 'utils/constants';
|
||||||
|
|
||||||
|
type UserProfileValue = Value & UserProfile;
|
||||||
|
type GroupValue = Value & Group;
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
group: GroupValue;
|
||||||
|
isSelected: boolean;
|
||||||
|
rowSelected: string;
|
||||||
|
selectedItemRef: React.RefObject<HTMLDivElement>;
|
||||||
|
onMouseMove: (group: GroupValue) => void;
|
||||||
|
addUserProfile: (profile: UserProfileValue) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayNameGetter = makeDisplayNameGetter();
|
||||||
|
|
||||||
|
const GroupOption = (props: Props) => {
|
||||||
|
const {
|
||||||
|
group,
|
||||||
|
isSelected,
|
||||||
|
rowSelected,
|
||||||
|
selectedItemRef,
|
||||||
|
onMouseMove,
|
||||||
|
addUserProfile,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const getProfilesByIdsAndUsernames = makeGetProfilesByIdsAndUsernames();
|
||||||
|
|
||||||
|
const profiles = useSelector((state: GlobalState) => getProfilesByIdsAndUsernames(state, {allUserIds: group.member_ids || [], allUsernames: []}) as UserProfileValue[]);
|
||||||
|
const overflowNames = useSelector((state: GlobalState) => {
|
||||||
|
if (group?.member_ids) {
|
||||||
|
return group?.member_ids.map((userId) => displayNameGetter(state, true)(getUser(state, userId))).join(', ');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const onAdd = useCallback(() => {
|
||||||
|
for (const profile of profiles) {
|
||||||
|
addUserProfile(profile);
|
||||||
|
}
|
||||||
|
}, [addUserProfile, profiles]);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
|
if (e.key === Constants.KeyCodes.ENTER[0] && isSelected) {
|
||||||
|
e.stopPropagation();
|
||||||
|
onAdd();
|
||||||
|
}
|
||||||
|
}, [isSelected, onAdd]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Bind the event listener
|
||||||
|
document.addEventListener('keydown', onKeyDown, true);
|
||||||
|
return () => {
|
||||||
|
// Unbind the event listener on clean up
|
||||||
|
document.removeEventListener('keydown', onKeyDown, true);
|
||||||
|
};
|
||||||
|
}, [onKeyDown]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={group.id}
|
||||||
|
ref={isSelected ? selectedItemRef : undefined}
|
||||||
|
className={'more-modal__row clickable ' + rowSelected}
|
||||||
|
onClick={onAdd}
|
||||||
|
onMouseMove={() => onMouseMove(group)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className='more-modal__group-image'
|
||||||
|
>
|
||||||
|
<AccountMultipleOutlineIcon
|
||||||
|
size={16}
|
||||||
|
color={'rgba(var(--center-channel-color-rgb), 0.56)'}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div className='more-modal__details'>
|
||||||
|
<div className='more-modal__name'>
|
||||||
|
<span className='group-display-name'>
|
||||||
|
{group.display_name}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className='ml-2 light group-name'
|
||||||
|
>
|
||||||
|
{'@'}{group.name}
|
||||||
|
</span>
|
||||||
|
<SimpleTooltip
|
||||||
|
id={'usernames-overflow'}
|
||||||
|
content={overflowNames}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className='add-group-members'
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='multiselect.addGroupMembers'
|
||||||
|
defaultMessage='Add {number} people'
|
||||||
|
values={{
|
||||||
|
number: group.member_count,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ChevronRightIcon
|
||||||
|
size={20}
|
||||||
|
color={'var(--link-color)'}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</SimpleTooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(GroupOption);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export {default} from './group_option';
|
||||||
@@ -5,37 +5,39 @@ import {connect} from 'react-redux';
|
|||||||
import {bindActionCreators} from 'redux';
|
import {bindActionCreators} from 'redux';
|
||||||
import type {ActionCreatorsMapObject, Dispatch} from 'redux';
|
import type {ActionCreatorsMapObject, Dispatch} from 'redux';
|
||||||
|
|
||||||
|
import type {GroupSearchParams} from '@mattermost/types/groups';
|
||||||
|
import type {TeamMembership} from '@mattermost/types/teams';
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
import type {RelationOneToOne} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
import {getTeamStats} from 'mattermost-redux/actions/teams';
|
import {getTeamStats, getTeamMembersByIds} from 'mattermost-redux/actions/teams';
|
||||||
import {getProfilesNotInChannel, getProfilesInChannel, searchProfiles} from 'mattermost-redux/actions/users';
|
import {getProfilesNotInChannel, getProfilesInChannel, searchProfiles} from 'mattermost-redux/actions/users';
|
||||||
import {Permissions} from 'mattermost-redux/constants';
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
import {getRecentProfilesFromDMs} from 'mattermost-redux/selectors/entities/channels';
|
import {getRecentProfilesFromDMs} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
import {makeGetAllAssociatedGroupsForReference} from 'mattermost-redux/selectors/entities/groups';
|
||||||
|
import {getTeammateNameDisplaySetting, isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
import {getCurrentTeam, getTeam} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeam, getMembersInCurrentTeam, getMembersInTeam, getTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getProfilesNotInCurrentChannel, getProfilesInCurrentChannel, getProfilesNotInCurrentTeam, getProfilesNotInTeam, getUserStatuses, makeGetProfilesNotInChannel, makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
|
import {getProfilesNotInCurrentChannel, getProfilesInCurrentChannel, getProfilesNotInCurrentTeam, getProfilesNotInTeam, getUserStatuses, makeGetProfilesNotInChannel, makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
|
||||||
import type {Action, ActionResult} from 'mattermost-redux/types/actions';
|
import type {Action, ActionResult} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import {addUsersToChannel} from 'actions/channel_actions';
|
import {addUsersToChannel} from 'actions/channel_actions';
|
||||||
import {loadStatusesForProfilesList} from 'actions/status_actions';
|
import {loadStatusesForProfilesList} from 'actions/status_actions';
|
||||||
|
import {searchAssociatedGroupsForReference} from 'actions/views/group';
|
||||||
import {closeModal} from 'actions/views/modals';
|
import {closeModal} from 'actions/views/modals';
|
||||||
|
|
||||||
import type {Value} from 'components/multiselect/multiselect';
|
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
import ChannelInviteModal from './channel_invite_modal';
|
import ChannelInviteModal from './channel_invite_modal';
|
||||||
|
|
||||||
type UserProfileValue = Value & UserProfile;
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeMapStateToProps(initialState: GlobalState, initialProps: OwnProps) {
|
function makeMapStateToProps(initialState: GlobalState, initialProps: OwnProps) {
|
||||||
|
const getAllAssociatedGroupsForReference = makeGetAllAssociatedGroupsForReference();
|
||||||
let doGetProfilesNotInChannel: (state: GlobalState, channelId: string, filters?: any) => UserProfile[];
|
let doGetProfilesNotInChannel: (state: GlobalState, channelId: string, filters?: any) => UserProfile[];
|
||||||
if (initialProps.channelId && initialProps.teamId) {
|
if (initialProps.channelId && initialProps.teamId) {
|
||||||
doGetProfilesNotInChannel = makeGetProfilesNotInChannel();
|
doGetProfilesNotInChannel = makeGetProfilesNotInChannel();
|
||||||
@@ -47,18 +49,21 @@ function makeMapStateToProps(initialState: GlobalState, initialProps: OwnProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (state: GlobalState, props: OwnProps) => {
|
return (state: GlobalState, props: OwnProps) => {
|
||||||
let profilesNotInCurrentChannel: UserProfileValue[];
|
let profilesNotInCurrentChannel: UserProfile[];
|
||||||
let profilesInCurrentChannel: UserProfileValue[];
|
let profilesInCurrentChannel: UserProfile[];
|
||||||
let profilesNotInCurrentTeam: UserProfileValue[];
|
let profilesNotInCurrentTeam: UserProfile[];
|
||||||
|
let membersInTeam: RelationOneToOne<UserProfile, TeamMembership>;
|
||||||
|
|
||||||
if (props.channelId && props.teamId) {
|
if (props.channelId && props.teamId) {
|
||||||
profilesNotInCurrentChannel = doGetProfilesNotInChannel(state, props.channelId) as UserProfileValue[];
|
profilesNotInCurrentChannel = doGetProfilesNotInChannel(state, props.channelId);
|
||||||
profilesInCurrentChannel = doGetProfilesInChannel(state, props.channelId) as UserProfileValue[];
|
profilesInCurrentChannel = doGetProfilesInChannel(state, props.channelId);
|
||||||
profilesNotInCurrentTeam = getProfilesNotInTeam(state, props.teamId) as UserProfileValue[];
|
profilesNotInCurrentTeam = getProfilesNotInTeam(state, props.teamId);
|
||||||
|
membersInTeam = getMembersInTeam(state, props.teamId);
|
||||||
} else {
|
} else {
|
||||||
profilesNotInCurrentChannel = getProfilesNotInCurrentChannel(state) as UserProfileValue[];
|
profilesNotInCurrentChannel = getProfilesNotInCurrentChannel(state);
|
||||||
profilesInCurrentChannel = getProfilesInCurrentChannel(state) as UserProfileValue[];
|
profilesInCurrentChannel = getProfilesInCurrentChannel(state);
|
||||||
profilesNotInCurrentTeam = getProfilesNotInCurrentTeam(state) as UserProfileValue[];
|
profilesNotInCurrentTeam = getProfilesNotInCurrentTeam(state);
|
||||||
|
membersInTeam = getMembersInCurrentTeam(state);
|
||||||
}
|
}
|
||||||
const profilesFromRecentDMs = getRecentProfilesFromDMs(state);
|
const profilesFromRecentDMs = getRecentProfilesFromDMs(state);
|
||||||
const config = getConfig(state);
|
const config = getConfig(state);
|
||||||
@@ -71,20 +76,27 @@ function makeMapStateToProps(initialState: GlobalState, initialProps: OwnProps)
|
|||||||
const isLicensed = license && license.IsLicensed === 'true';
|
const isLicensed = license && license.IsLicensed === 'true';
|
||||||
const isGroupConstrained = Boolean(currentTeam.group_constrained);
|
const isGroupConstrained = Boolean(currentTeam.group_constrained);
|
||||||
const canInviteGuests = !isGroupConstrained && isLicensed && guestAccountsEnabled && haveICurrentTeamPermission(state, Permissions.INVITE_GUEST);
|
const canInviteGuests = !isGroupConstrained && isLicensed && guestAccountsEnabled && haveICurrentTeamPermission(state, Permissions.INVITE_GUEST);
|
||||||
|
const enableCustomUserGroups = isCustomGroupsEnabled(state);
|
||||||
|
|
||||||
|
const isGroupsEnabled = enableCustomUserGroups || (license?.IsLicensed === 'true' && license?.LDAPGroups === 'true');
|
||||||
|
|
||||||
const userStatuses = getUserStatuses(state);
|
const userStatuses = getUserStatuses(state);
|
||||||
|
|
||||||
const teammateNameDisplaySetting = getTeammateNameDisplaySetting(state);
|
const teammateNameDisplaySetting = getTeammateNameDisplaySetting(state);
|
||||||
|
const groups = getAllAssociatedGroupsForReference(state, true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
profilesNotInCurrentChannel,
|
profilesNotInCurrentChannel,
|
||||||
profilesInCurrentChannel,
|
profilesInCurrentChannel,
|
||||||
profilesNotInCurrentTeam,
|
profilesNotInCurrentTeam,
|
||||||
|
membersInTeam,
|
||||||
teammateNameDisplaySetting,
|
teammateNameDisplaySetting,
|
||||||
profilesFromRecentDMs,
|
profilesFromRecentDMs,
|
||||||
userStatuses,
|
userStatuses,
|
||||||
canInviteGuests,
|
canInviteGuests,
|
||||||
emailInvitationsEnabled,
|
emailInvitationsEnabled,
|
||||||
|
groups,
|
||||||
|
isGroupsEnabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -97,6 +109,8 @@ type Actions = {
|
|||||||
searchProfiles: (term: string, options: any) => Promise<ActionResult>;
|
searchProfiles: (term: string, options: any) => Promise<ActionResult>;
|
||||||
closeModal: (modalId: string) => void;
|
closeModal: (modalId: string) => void;
|
||||||
getProfilesInChannel: (channelId: string, page: number, perPage: number, sort: string, options: {active?: boolean}) => Promise<ActionResult>;
|
getProfilesInChannel: (channelId: string, page: number, perPage: number, sort: string, options: {active?: boolean}) => Promise<ActionResult>;
|
||||||
|
searchAssociatedGroupsForReference: (prefix: string, teamId: string, channelId: string | undefined, opts: GroupSearchParams) => Promise<ActionResult>;
|
||||||
|
getTeamMembersByIds: (teamId: string, userIds: string[]) => Promise<ActionResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
@@ -109,6 +123,8 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
loadStatusesForProfilesList,
|
loadStatusesForProfilesList,
|
||||||
searchProfiles,
|
searchProfiles,
|
||||||
closeModal,
|
closeModal,
|
||||||
|
searchAssociatedGroupsForReference,
|
||||||
|
getTeamMembersByIds,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -0,0 +1,4 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export {default} from './team_warning_banner';
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {Provider} from 'react-redux';
|
||||||
|
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import TeamWarningBanner from 'components/channel_invite_modal/team_warning_banner/team_warning_banner';
|
||||||
|
import type {Value} from 'components/multiselect/multiselect';
|
||||||
|
|
||||||
|
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||||
|
import mockStore from 'tests/test_store';
|
||||||
|
|
||||||
|
type UserProfileValue = Value & UserProfile;
|
||||||
|
|
||||||
|
jest.mock('utils/utils', () => {
|
||||||
|
const original = jest.requireActual('utils/utils');
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
localizeMessage: jest.fn(),
|
||||||
|
sortUsersAndGroups: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function createUsers(count: number): UserProfileValue[] {
|
||||||
|
const users: UserProfileValue[] = [];
|
||||||
|
for (let x = 0; x < count; x++) {
|
||||||
|
const user = {
|
||||||
|
id: 'user-' + x,
|
||||||
|
username: 'user-' + x,
|
||||||
|
label: 'user-' + x,
|
||||||
|
value: 'user-' + x,
|
||||||
|
delete_at: 0,
|
||||||
|
} as UserProfileValue;
|
||||||
|
users.push(user);
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('components/channel_invite_modal/team_warning_banner', () => {
|
||||||
|
const teamId = 'team1';
|
||||||
|
const state = {
|
||||||
|
entities: {
|
||||||
|
channels: {},
|
||||||
|
teams: {
|
||||||
|
current: {id: 'team1'},
|
||||||
|
teams: {
|
||||||
|
team1: {
|
||||||
|
id: 'team1',
|
||||||
|
display_name: 'Team Name Display',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
myPreferences: {},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
currentUserId: 'admin1',
|
||||||
|
profiles: {},
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
myGroups: {},
|
||||||
|
groups: {},
|
||||||
|
},
|
||||||
|
emojis: {
|
||||||
|
customEmoji: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
// beforeEach(() => {
|
||||||
|
// state = {...state};
|
||||||
|
// });
|
||||||
|
|
||||||
|
test('should return empty snapshot', () => {
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<Provider store={store}>
|
||||||
|
<TeamWarningBanner
|
||||||
|
teamId={teamId}
|
||||||
|
users={[]}
|
||||||
|
guests={[]}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match snapshot for team_warning_banner with > 10 profiles', () => {
|
||||||
|
const users = createUsers(11);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<Provider store={store}>
|
||||||
|
<TeamWarningBanner
|
||||||
|
teamId={teamId}
|
||||||
|
users={users}
|
||||||
|
guests={[]}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match snapshot for team_warning_banner with < 10 profiles', () => {
|
||||||
|
const users = createUsers(2);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<Provider store={store}>
|
||||||
|
<TeamWarningBanner
|
||||||
|
teamId={teamId}
|
||||||
|
users={users}
|
||||||
|
guests={[]}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match snapshot for team_warning_banner with > 10 guest profiles', () => {
|
||||||
|
const guests = createUsers(11);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<Provider store={store}>
|
||||||
|
<TeamWarningBanner
|
||||||
|
teamId={teamId}
|
||||||
|
users={[]}
|
||||||
|
guests={guests}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match snapshot for team_warning_banner with < 10 guest profiles', () => {
|
||||||
|
const guests = createUsers(2);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<Provider store={store}>
|
||||||
|
<TeamWarningBanner
|
||||||
|
teamId={teamId}
|
||||||
|
users={[]}
|
||||||
|
guests={guests}
|
||||||
|
/>
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback} from 'react';
|
||||||
|
import {FormattedMessage, FormattedList, useIntl} from 'react-intl';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import type {GlobalState} from '@mattermost/types/store';
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {getTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||||
|
|
||||||
|
import AlertBanner from 'components/alert_banner';
|
||||||
|
import AtMention from 'components/at_mention';
|
||||||
|
import type {Value} from 'components/multiselect/multiselect';
|
||||||
|
import SimpleTooltip from 'components/widgets/simple_tooltip';
|
||||||
|
|
||||||
|
import {t} from 'utils/i18n';
|
||||||
|
|
||||||
|
type UserProfileValue = Value & UserProfile;
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
teamId: string;
|
||||||
|
users: UserProfileValue[];
|
||||||
|
guests: UserProfileValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const TeamWarningBanner = (props: Props) => {
|
||||||
|
const {
|
||||||
|
teamId,
|
||||||
|
users,
|
||||||
|
guests,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
const team = useSelector((state: GlobalState) => getTeam(state, teamId));
|
||||||
|
|
||||||
|
const getCommaSeparatedUsernames = useCallback((users: Array<UserProfileValue | UserProfile>) => {
|
||||||
|
return users.map((user) => {
|
||||||
|
return `@${user.username}`;
|
||||||
|
}).join(', ');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getGuestMessage = useCallback(() => {
|
||||||
|
if (guests.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commaSeparatedUsernames = getCommaSeparatedUsernames(guests);
|
||||||
|
const firstName = guests[0].username;
|
||||||
|
if (guests.length > 10) {
|
||||||
|
return (
|
||||||
|
formatMessage(
|
||||||
|
{
|
||||||
|
id: t('channel_invite.invite_team_members.guests.messageOverflow'),
|
||||||
|
defaultMessage: '{firstUser} and {others} are guest users and need to first be invited to the team before you can add them to the channel. Once they\'ve joined the team, you can add them to this channel.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
firstUser: (
|
||||||
|
<AtMention
|
||||||
|
key={firstName}
|
||||||
|
mentionName={firstName}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
others: (
|
||||||
|
<SimpleTooltip
|
||||||
|
id={'usernames-overflow'}
|
||||||
|
content={commaSeparatedUsernames.replace(`@${firstName}, `, '')}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className='add-others-link'
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_invite.invite_team_members.messageOthers'
|
||||||
|
defaultMessage='{count} others'
|
||||||
|
values={{
|
||||||
|
count: guests.length - 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</SimpleTooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guestsList = guests.map((user) => {
|
||||||
|
return (
|
||||||
|
<AtMention
|
||||||
|
key={user.username}
|
||||||
|
mentionName={user.username}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
formatMessage(
|
||||||
|
{
|
||||||
|
id: t('channel_invite.invite_team_members.guests.message'),
|
||||||
|
defaultMessage: '{count, plural, =1 {{firstUser} is a guest user and needs} other {{users} are guest users and need}} to first be invited to the team before you can add them to the channel. Once they\'ve joined the team, you can add them to this channel.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: guests.length,
|
||||||
|
users: (<FormattedList value={guestsList}/>),
|
||||||
|
firstUser: (
|
||||||
|
<AtMention
|
||||||
|
key={firstName}
|
||||||
|
mentionName={firstName}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
team: (<strong>{team.display_name}</strong>),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [guests, formatMessage, getCommaSeparatedUsernames, team.display_name]);
|
||||||
|
|
||||||
|
const getMessage = useCallback(() => {
|
||||||
|
const commaSeparatedUsernames = getCommaSeparatedUsernames(users);
|
||||||
|
const firstName = users[0].username;
|
||||||
|
|
||||||
|
if (users.length > 10) {
|
||||||
|
return formatMessage(
|
||||||
|
{
|
||||||
|
id: t('channel_invite.invite_team_members.messageOverflow'),
|
||||||
|
defaultMessage: 'You can add {firstUser} and {others} to this channel once they are members of the {team} team.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
firstUser: (
|
||||||
|
<AtMention
|
||||||
|
key={firstName}
|
||||||
|
mentionName={firstName}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
others: (
|
||||||
|
<SimpleTooltip
|
||||||
|
id={'usernames-overflow'}
|
||||||
|
content={commaSeparatedUsernames.replace(`@${firstName}, `, '')}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className='add-others-link'
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_invite.invite_team_members.messageOthers'
|
||||||
|
defaultMessage='{count} others'
|
||||||
|
values={{
|
||||||
|
count: users.length - 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</SimpleTooltip>
|
||||||
|
),
|
||||||
|
team: (<strong>{team.display_name}</strong>),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersList = users.map((user) => {
|
||||||
|
return (
|
||||||
|
<AtMention
|
||||||
|
key={user.username}
|
||||||
|
mentionName={user.username}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
formatMessage(
|
||||||
|
{
|
||||||
|
id: t('channel_invite.invite_team_members.message'),
|
||||||
|
defaultMessage: 'You can add {count, plural, =1 {{firstUser}} other {{users}}} to this channel once they are members of the {team} team.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: users.length,
|
||||||
|
users: (<FormattedList value={usersList}/>),
|
||||||
|
firstUser: (
|
||||||
|
<AtMention
|
||||||
|
key={firstName}
|
||||||
|
mentionName={firstName}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
team: (<strong>{team.display_name}</strong>),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [users, getCommaSeparatedUsernames, team, formatMessage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
(users.length > 0 || guests.length > 0) &&
|
||||||
|
<AlertBanner
|
||||||
|
id='teamWarningBanner'
|
||||||
|
mode='warning'
|
||||||
|
variant='app'
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_invite.invite_team_members.title'
|
||||||
|
defaultMessage='{count, plural, =1 {1 user was} other {# users were}} not selected because they are not a part of this team'
|
||||||
|
values={{
|
||||||
|
count: users.length + guests.length,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
message={
|
||||||
|
users.length > 0 &&
|
||||||
|
getMessage()
|
||||||
|
}
|
||||||
|
footerMessage={
|
||||||
|
guests.length > 0 &&
|
||||||
|
getGuestMessage()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TeamWarningBanner);
|
||||||
@@ -7,13 +7,28 @@ import {VariableSizeList} from 'react-window';
|
|||||||
import type {ListChildComponentProps} from 'react-window';
|
import type {ListChildComponentProps} from 'react-window';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import InfiniteLoader from 'react-window-infinite-loader';
|
||||||
|
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
import {ListItemType} from './channel_members_rhs';
|
|
||||||
import type {ChannelMember, ListItem} from './channel_members_rhs';
|
|
||||||
import Member from './member';
|
import Member from './member';
|
||||||
|
|
||||||
|
interface ChannelMember {
|
||||||
|
user: UserProfile;
|
||||||
|
membership?: ChannelMembership;
|
||||||
|
status?: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ListItemType {
|
||||||
|
Member = 'member',
|
||||||
|
FirstSeparator = 'first-separator',
|
||||||
|
Separator = 'separator',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListItem {
|
||||||
|
type: ListItemType;
|
||||||
|
data: ChannelMember | JSX.Element;
|
||||||
|
}
|
||||||
export interface Props {
|
export interface Props {
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
members: ListItem[];
|
members: ListItem[];
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export function initializeTeam(team: Team): ActionFunc<Team, ServerError> {
|
|||||||
page: 0,
|
page: 0,
|
||||||
per_page: 60,
|
per_page: 60,
|
||||||
include_member_count: true,
|
include_member_count: true,
|
||||||
|
include_member_ids: true,
|
||||||
include_archived: false,
|
include_archived: false,
|
||||||
};
|
};
|
||||||
const myGroupsParams: GetGroupsForUserParams = {
|
const myGroupsParams: GetGroupsForUserParams = {
|
||||||
|
|||||||
@@ -2930,6 +2930,12 @@
|
|||||||
"channel_info_rhs.top_buttons.muted": "Muted",
|
"channel_info_rhs.top_buttons.muted": "Muted",
|
||||||
"channel_invite.addNewMembers": "Add people to {channel}",
|
"channel_invite.addNewMembers": "Add people to {channel}",
|
||||||
"channel_invite.invite_guest": "Invite as a Guest",
|
"channel_invite.invite_guest": "Invite as a Guest",
|
||||||
|
"channel_invite.invite_team_members.guests.message": "{count, plural, =1 {{firstUser} is a guest user and needs} other {{users} are guest users and need}} to first be invited to the team before you can add them to the channel. Once they've joined the team, you can add them to this channel.",
|
||||||
|
"channel_invite.invite_team_members.guests.messageOverflow": "{firstUser} and {others} are guest users and need to first be invited to the team before you can add them to the channel. Once they've joined the team, you can add them to this channel.",
|
||||||
|
"channel_invite.invite_team_members.message": "You can add {count, plural, =1 {{firstUser}} other {{users}}} to this channel once they are members of the {team} team.",
|
||||||
|
"channel_invite.invite_team_members.messageOthers": "{count} others",
|
||||||
|
"channel_invite.invite_team_members.messageOverflow": "You can add {firstUser} and {others} to this channel once they are members of the {team} team.",
|
||||||
|
"channel_invite.invite_team_members.title": "{count, plural, =1 {1 user was} other {# users were}} not selected because they are not a part of this team",
|
||||||
"channel_invite.no_options_message": "No matches found - <InvitationModalLink>Invite them to the team</InvitationModalLink>",
|
"channel_invite.no_options_message": "No matches found - <InvitationModalLink>Invite them to the team</InvitationModalLink>",
|
||||||
"channel_loader.posted": "Posted",
|
"channel_loader.posted": "Posted",
|
||||||
"channel_loader.postedImage": " posted an image",
|
"channel_loader.postedImage": " posted an image",
|
||||||
@@ -4045,6 +4051,7 @@
|
|||||||
"msg_typing.isTyping": "{user} is typing...",
|
"msg_typing.isTyping": "{user} is typing...",
|
||||||
"multiselect.add": "Add",
|
"multiselect.add": "Add",
|
||||||
"multiselect.addChannelsPlaceholder": "Search and add channels",
|
"multiselect.addChannelsPlaceholder": "Search and add channels",
|
||||||
|
"multiselect.addGroupMembers": "Add {number} people",
|
||||||
"multiselect.addGroupsPlaceholder": "Search and add groups",
|
"multiselect.addGroupsPlaceholder": "Search and add groups",
|
||||||
"multiselect.adding": "Adding...",
|
"multiselect.adding": "Adding...",
|
||||||
"multiselect.addPeopleToGroup": "Add People",
|
"multiselect.addPeopleToGroup": "Add People",
|
||||||
@@ -4063,6 +4070,7 @@
|
|||||||
"multiselect.numPeopleRemaining": "Use ↑↓ to browse, ↵ to select. You can add {num, number} more {num, plural, one {person} other {people}}. ",
|
"multiselect.numPeopleRemaining": "Use ↑↓ to browse, ↵ to select. You can add {num, number} more {num, plural, one {person} other {people}}. ",
|
||||||
"multiselect.numRemaining": "Up to {max, number} can be added at a time. You have {num, number} remaining.",
|
"multiselect.numRemaining": "Up to {max, number} can be added at a time. You have {num, number} remaining.",
|
||||||
"multiselect.placeholder": "Search for people",
|
"multiselect.placeholder": "Search for people",
|
||||||
|
"multiselect.placeholder.peopleOrGroups": "Search for people or groups",
|
||||||
"multiselect.saveDetailsButton": "Save Details",
|
"multiselect.saveDetailsButton": "Save Details",
|
||||||
"multiselect.savingDetailsButton": "Saving...",
|
"multiselect.savingDetailsButton": "Saving...",
|
||||||
"multiselect.selectChannels": "Use ↑↓ to browse, ↵ to select.",
|
"multiselect.selectChannels": "Use ↑↓ to browse, ↵ to select.",
|
||||||
|
|||||||
@@ -188,6 +188,15 @@ export const getMembersInCurrentTeam: (state: GlobalState) => RelationOneToOne<U
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getMembersInTeam: (state: GlobalState, teamId: string) => RelationOneToOne<UserProfile, TeamMembership> = createSelector(
|
||||||
|
'getMembersInTeam',
|
||||||
|
(state: GlobalState, teamId: string) => teamId,
|
||||||
|
getMembersInTeams,
|
||||||
|
(teamId, teamMembers) => {
|
||||||
|
return teamMembers[teamId];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export function getTeamMember(state: GlobalState, teamId: string, userId: string): TeamMembership | undefined {
|
export function getTeamMember(state: GlobalState, teamId: string, userId: string): TeamMembership | undefined {
|
||||||
return getMembersInTeams(state)[teamId]?.[userId];
|
return getMembersInTeams(state)[teamId]?.[userId];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,15 @@
|
|||||||
padding: 0 2.4rem;
|
padding: 0 2.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.AlertBanner {
|
||||||
|
margin: 1.2rem 2.4rem 0;
|
||||||
|
|
||||||
|
button.btn {
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.react-select__multi-value {
|
.react-select__multi-value {
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
}
|
}
|
||||||
@@ -92,6 +101,34 @@
|
|||||||
.more-modal__options {
|
.more-modal__options {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
|
|
||||||
|
.group-name {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-modal__group-image {
|
||||||
|
display: flex;
|
||||||
|
width: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-group-members {
|
||||||
|
display: none;
|
||||||
|
min-width: 110px;
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--button-bg);
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +161,10 @@
|
|||||||
.more-modal__details {
|
.more-modal__details {
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-others-link {
|
||||||
|
color: var(--button-bg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .channel-invite__content {
|
.modal .channel-invite__content {
|
||||||
|
|||||||
@@ -879,6 +879,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -966,6 +967,12 @@
|
|||||||
.more-modal__lastPostAt {
|
.more-modal__lastPostAt {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-modal__details {
|
||||||
|
.add-group-members {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-modal__shared-actions {
|
.more-modal__shared-actions {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {Channel} from '@mattermost/types/channels';
|
|||||||
import type {Address} from '@mattermost/types/cloud';
|
import type {Address} from '@mattermost/types/cloud';
|
||||||
import type {ClientConfig} from '@mattermost/types/config';
|
import type {ClientConfig} from '@mattermost/types/config';
|
||||||
import type {FileInfo} from '@mattermost/types/files';
|
import type {FileInfo} from '@mattermost/types/files';
|
||||||
|
import type {Group} from '@mattermost/types/groups';
|
||||||
import type {GlobalState} from '@mattermost/types/store';
|
import type {GlobalState} from '@mattermost/types/store';
|
||||||
import type {Team} from '@mattermost/types/teams';
|
import type {Team} from '@mattermost/types/teams';
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
@@ -1745,3 +1746,20 @@ export function getBlankAddressWithCountry(country?: string): Address {
|
|||||||
state: '',
|
state: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sortUsersAndGroups(a: UserProfile | Group, b: UserProfile | Group) {
|
||||||
|
let aSortString = '';
|
||||||
|
let bSortString = '';
|
||||||
|
if ('username' in a) {
|
||||||
|
aSortString = a.username;
|
||||||
|
} else {
|
||||||
|
aSortString = a.name;
|
||||||
|
}
|
||||||
|
if ('username' in b) {
|
||||||
|
bSortString = b.username;
|
||||||
|
} else {
|
||||||
|
bSortString = b.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return aSortString.localeCompare(bSortString);
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export type Group = {
|
|||||||
allow_reference: boolean;
|
allow_reference: boolean;
|
||||||
channel_member_count?: number;
|
channel_member_count?: number;
|
||||||
channel_member_timezones_count?: number;
|
channel_member_timezones_count?: number;
|
||||||
|
member_ids?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum GroupSource {
|
export enum GroupSource {
|
||||||
@@ -157,6 +158,7 @@ export type GetGroupsParams = {
|
|||||||
include_member_count?: boolean;
|
include_member_count?: boolean;
|
||||||
include_archived?: boolean;
|
include_archived?: boolean;
|
||||||
filter_archived?: boolean;
|
filter_archived?: boolean;
|
||||||
|
include_member_ids?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetGroupsForUserParams = GetGroupsParams & {
|
export type GetGroupsForUserParams = GetGroupsParams & {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user