[MM-55277] Team's menu doesn't follow standard accessible keyboard behavior (#29917)

Этот коммит содержится в:
M-ZubairAhmed
2025-04-18 19:18:54 +05:30
коммит произвёл GitHub
родитель b59f10cbbd
Коммит 502506a517
130 изменённых файлов: 2208 добавлений и 5137 удалений

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

@@ -105,9 +105,9 @@ describe('Verify Accessibility Support in Dropdown Menus', () => {
{id: 'invitePeople', label: 'Invite People dialog'},
{id: 'teamSettings', label: 'Team Settings dialog'},
{id: 'manageMembers', label: 'Manage Members dialog'},
{id: 'joinTeam', text: 'Join Another Team'},
{id: 'joinTeam', text: 'Join another team'},
{id: 'leaveTeam', label: 'Leave Team dialog'},
{id: 'createTeam', text: 'Create a Team'},
{id: 'createTeam', text: 'Create a team'},
];
menuItems.forEach((item) => {

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

@@ -70,7 +70,7 @@ describe('Profile > Profile Settings> Full Name', () => {
it('MM-T2048 Empty full name: @ still displays before username', () => {
// # Open any user list ("View Members", "Add Members", "Manage Members", ..)
cy.uiOpenTeamMenu('View Members');
cy.uiOpenTeamMenu('View members');
// # Find a user who hasn't set their full name
cy.get('.modal-title').should('be.visible');

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

@@ -39,7 +39,7 @@ describe('Settings > Sidebar > General', () => {
});
// # Open team menu and click "View Members"
cy.uiOpenTeamMenu('View Members');
cy.uiOpenTeamMenu('View members');
// # Search for username and check that no nickname is present
cy.get('.modal-title').should('be.visible');
@@ -68,7 +68,7 @@ describe('Settings > Sidebar > General', () => {
});
// # Open team menu and click "View Members"
cy.uiOpenTeamMenu('View Members');
cy.uiOpenTeamMenu('View members');
// # Search for username and check that expected nickname is present
cy.get('.modal-title').should('be.visible');

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

@@ -67,7 +67,7 @@ describe('Leave an archived channel', () => {
cy.uiArchiveChannel();
// # Open channel menu and click View Members
cy.uiOpenChannelMenu('View Members');
cy.uiOpenChannelMenu('View members');
// * Channel Members modal opens
cy.get('div#channelMembersModal').should('be.visible');

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

@@ -267,7 +267,7 @@ describe('Authentication', () => {
cy.postMessage('hello');
// # Open team menu and click on "Invite People"
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Click invite members if needed
cy.findByText('Copy invite link').click();

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

@@ -72,7 +72,7 @@ describe('Bots in lists', () => {
// # Open channel members
cy.get('.channel-header__trigger').click();
cy.findByText('Manage Members').click();
cy.findByText('Manage members').click();
cy.get('.more-modal__row .more-modal__name').then(async ($query) => {
// # Extract usernames from jQuery collection

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

@@ -37,7 +37,7 @@ describe('Managing bots in Teams and Channels', () => {
await client.addToTeam(team.id, bot.user_id);
// # Open team menu and click 'Manage Members'
cy.uiOpenTeamMenu('Manage Members');
cy.uiOpenTeamMenu('Manage members');
// # Find bot
cy.get('.more-modal__list').find('.more-modal__row').its('length').should('be.gt', 0);

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

@@ -105,7 +105,7 @@ describe('Custom Status - Verifying Where Custom Status Appears', () => {
it('MM-T3850_8 should show custom status emoji next to username in the team members modal', () => {
// # Open team menu and click on "View Members"
cy.uiOpenTeamMenu('View Members');
cy.uiOpenTeamMenu('View members');
cy.get('#teamMembersModal').should('exist');
// # Search the current user's username in the search input

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

@@ -36,7 +36,7 @@ describe('Verify Accessibility Support in different input fields', () => {
it('MM-T1456 Verify Accessibility Support in Input fields in Invite People Flow', () => {
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Click invite members if needed
cy.get('.InviteAs').findByTestId('inviteMembersLink').click();

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

@@ -47,7 +47,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
verifyUserMenuModal('Profile');
// * Verify the accessibility support in Team Settings Dialog
verifyMainMenuModal('Team Settings');
verifyMainMenuModal('Team settings');
// * Verify the accessibility support in Manage Members Dialog
verifyMainMenuModal('Manage Members', `${testTeam.display_name} Members`);
@@ -72,7 +72,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
// # Open Channel Members Dialog
cy.get('#channelHeaderTitle').click();
cy.findByText('Manage Members').click().wait(TIMEOUTS.FIVE_SEC);
cy.findByText('Manage members').click().wait(TIMEOUTS.FIVE_SEC);
// * Verify the accessibility support in Manage Members Dialog
cy.findByRole('dialog', {name: 'Off-Topic Members'}).within(() => {

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

@@ -44,7 +44,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
});
it.skip('MM-T1466 Accessibility Support in Direct Messages Dialog screen', () => {
it('MM-T1466 Accessibility Support in Direct Messages Dialog screen', () => {
// * Verify the aria-label in create direct message button
cy.uiAddDirectMessage().click();
@@ -53,11 +53,11 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
cy.findByRole('heading', {name: 'Direct Messages'});
// * Verify the accessibility support in search input
cy.findByRole('textbox', {name: 'Search for people'}).
cy.findByLabelText('Search for people').
should('have.attr', 'aria-autocomplete', 'list');
// # Search for a text and then check up and down arrow
cy.findByRole('textbox', {name: 'Search for people'}).
cy.findByLabelText('Search for people').
typeWithForce('s').
wait(TIMEOUTS.HALF_SEC).
typeWithForce('{downarrow}{downarrow}{downarrow}{uparrow}');
@@ -80,7 +80,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
// # Search for an invalid text
const additionalSearchTerm = 'somethingwhichdoesnotexist';
cy.findByRole('textbox', {name: 'Search for people'}).clear().
cy.findByLabelText('Search for people').clear().
typeWithForce(additionalSearchTerm).
wait(TIMEOUTS.HALF_SEC);
@@ -137,7 +137,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
});
});
it.skip('MM-T1468 Accessibility Support in Add people to Channel Dialog screen', () => {
it('MM-T1468 Accessibility Support in Add people to Channel Dialog screen', () => {
// # Add atleast 5 users
for (let i = 0; i < 5; i++) {
cy.apiCreateUser().then(({user}) => { // eslint-disable-line
@@ -159,11 +159,11 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
cy.wait(TIMEOUTS.ONE_SEC);
// * Verify the accessibility support in search input
cy.findByRole('textbox', {name: 'Search for people or groups'}).
cy.findByLabelText('Search for people or groups').
should('have.attr', 'aria-autocomplete', 'list');
// # Search for a text and then check up and down arrow
cy.findByRole('textbox', {name: 'Search for people or groups'}).
cy.findByLabelText('Search for people or groups').
wait(TIMEOUTS.HALF_SEC).
typeWithForce('u').
wait(TIMEOUTS.HALF_SEC).
@@ -189,7 +189,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
});
// # Search for an invalid text and check if reader can read no results
cy.findByRole('textbox', {name: 'Search for people or groups'}).
cy.findByLabelText('Search for people or groups').
typeWithForce('somethingwhichdoesnotexist').
wait(TIMEOUTS.HALF_SEC);
@@ -203,7 +203,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
it('MM-T1515 Verify Accessibility Support in Invite People Flow', () => {
// # Open Invite People
cy.uiGetLHSHeader().click();
cy.get('#invitePeople').should('be.visible').click();
cy.get("#sidebarTeamMenu li:contains('Invite people')").should('be.visible').click();
// * Verify accessibility support in Invite People Dialog
cy.findByTestId('invitationModal').should('have.attr', 'aria-modal', 'true').and('have.attr', 'aria-labelledby', 'invitation_modal_title').and('have.attr', 'role', 'dialog');

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

@@ -111,9 +111,14 @@ function userGroupsNotification() {
}
function creatNewTeamNotification() {
cy.get('.test-team-header').click().then(() => {
// # Click on the team menu button
cy.get('#sidebarTeamMenuButton').click();
// # Click on the lock button on Create a team menu item
cy.get('#sidebarTeamMenu').within(() => {
cy.get('#mattermost_feature_create_multiple_teams-restricted-indicator').click();
});
cy.get('#FeatureRestrictedModal').should('exist');
cy.get('#button-plans').as('notifyButton').should('have.text', 'Notify admin').click();
cy.get('@notifyButton').should('have.text', 'Admin notified!');

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

@@ -64,24 +64,27 @@ describe('Guest Account - Guest User Experience', () => {
// * Verify reduced options in Team Menu
const missingMainOptions = [
'Invite People',
'Team Settings',
'Manage Members',
'Join Another Team',
'Create a Team',
'Invite people',
'Team settings',
'Manage members',
'Join another team',
'Create a team',
];
missingMainOptions.forEach((missingOption) => {
cy.uiGetLHSTeamMenu().should('not.contain', missingOption);
});
const includeMainOptions = [
'View Members',
'Leave Team',
'View members',
'Leave team',
];
includeMainOptions.forEach((includeOption) => {
cy.uiGetLHSTeamMenu().findByText(includeOption);
});
// # Close the main menu
cy.get('body').type('{esc}');
// * Verify Reduced Options in LHS
cy.uiGetLHSAddChannelButton().should('not.exist');
@@ -120,7 +123,7 @@ describe('Guest Account - Guest User Experience', () => {
cy.uiGetLhsSection('CHANNELS').find('.SidebarChannel').should('have.length', 3);
// * Verify list of Users a Guest User can see in Team Members dialog
cy.uiOpenTeamMenu('View Members');
cy.uiOpenTeamMenu('View members');
cy.get('#searchableUserListTotal').should('be.visible').and('have.text', '1 - 2 members of 2 total');
});
@@ -136,17 +139,18 @@ describe('Guest Account - Guest User Experience', () => {
// * Verify options in team menu are changed
cy.uiOpenTeamMenu();
const includeOptions = [
'Invite People',
'View Members',
'Leave Team',
'Create a Team',
'Invite people',
'View members',
'Leave team',
'Create a team',
];
includeOptions.forEach((option) => {
cy.uiGetLHSTeamMenu().findByText(option);
});
// # Close the main menu
cy.uiGetLHSHeader().click();
// Close the main menu with Escape key
cy.get('body').type('{esc}');
cy.uiGetLHSTeamMenu().should('not.exist');
// * Verify Options in LHS are changed
cy.uiGetLHSAddChannelButton();

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

@@ -101,7 +101,7 @@ describe('Guest Accounts', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu, click Invite People, then invite guest
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
cy.findByTestId('inviteGuestLink').click();
// # Type guest user e-mail address.

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

@@ -68,7 +68,7 @@ describe('Verify Guest User Identification in different screens', () => {
it('Verify Guest Badge in Team Members dialog', () => {
// # Open team menu and click 'View Members'
cy.uiOpenTeamMenu('View Members');
cy.uiOpenTeamMenu('View members');
cy.get('#teamMembersModal').should('be.visible').within(($el) => {
cy.wrap($el).findAllByTestId('userListItemDetails').each(($elChild) => {

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

@@ -53,7 +53,7 @@ describe('Guest Account - Guest User Invitation Flow', () => {
it('MM-T4451 Verify UI Elements of Guest User Invitation Flow', () => {
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// * Verify Invite Guest link
cy.findByTestId('inviteGuestLink').should('be.visible').click();
@@ -117,7 +117,7 @@ describe('Guest Account - Guest User Invitation Flow', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// * Verify if Invite Members modal is displayed when guest account feature is disabled
cy.findByTestId('invitationModal').find('h1').should('have.text', `Invite people to ${testTeam.display_name}`);
@@ -164,7 +164,7 @@ describe('Guest Account - Guest User Invitation Flow', () => {
cy.apiDemoteUserToGuest(newUser.id);
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Click invite members if needed
cy.get('.InviteAs').findByTestId('inviteMembersLink').click();
@@ -226,7 +226,7 @@ describe('Guest Account - Guest User Invitation Flow', () => {
it('hides the copy link button when inviting guests', () => {
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Select Guest
cy.findByTestId('inviteGuestLink').should('be.visible').click();

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

@@ -16,7 +16,7 @@ export function changeGuestFeatureSettings(featureFlag = true, emailInvitation =
export function invitePeople(typeText: string, resultsCount: number, verifyText: string, channelName = 'Town Square', clickInvite = true) {
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Click on the next icon to invite guest
cy.findByTestId('inviteGuestLink').click();

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

@@ -52,7 +52,7 @@ describe('Guest Account - Member Invitation Flow', () => {
const email = `temp-${getRandomId()}@mattermost.com`;
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// * Verify UI Elements in initial step
cy.findByTestId('invitationModal').within(() => {
@@ -251,7 +251,7 @@ describe('Guest Account - Member Invitation Flow', () => {
function invitePeople(typeText, resultsCount, verifyText, clickInvite = true) {
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Search and add a member
cy.get('.users-emails-input__control').should('be.visible').within(() => {

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

@@ -103,7 +103,7 @@ describe('Guest Account - Verify Guest Access UI', () => {
});
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// * Verify that an option to Invite via Guest should not be available
cy.findByTestId('inviteGuestLink').should('not.exist');

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

@@ -219,7 +219,7 @@ describe('LDAP guest', () => {
cy.visit(`/${team.name}/channels/town-square`);
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
cy.wait(TIMEOUTS.TWO_SEC);

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

@@ -54,7 +54,7 @@ describe('Group Synced Team - Bot invitation flow', () => {
cy.visit(`/${groupConstrainedTeam.name}`);
// # Click 'Invite People' at team menu
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Type the first letters of a bot
cy.get('.users-emails-input__control input').typeWithForce(bot.username);
@@ -75,7 +75,7 @@ describe('Group Synced Team - Bot invitation flow', () => {
cy.visit(`/${groupConstrainedTeam.name}`);
// # Click 'Manage Members' at team menu
cy.uiOpenTeamMenu('Manage Members');
cy.uiOpenTeamMenu('Manage members');
// # Search for the bot that we want to remove
cy.get('#searchUsersInput').should('be.visible').type(bot.username);

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

@@ -95,10 +95,10 @@ describe('Team Permissions', () => {
cy.uiOpenTeamMenu().wait(TIMEOUTS.HALF_SEC);
// * Verify `Invite People` menu item is not present
cy.get('#invitePeople').should('not.exist');
cy.get("#sidebarTeamMenu li:contains('Invite people')").should('not.exist');
// # Click `View Members` menu item
cy.get('#viewMembers').should('be.visible').click().wait(TIMEOUTS.HALF_SEC);
cy.get("#sidebarTeamMenu li:contains('View members')").should('be.visible').click().wait(TIMEOUTS.HALF_SEC);
// * Verify team members modal opens
cy.get('#teamMembersModal').should('be.visible');

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

@@ -108,7 +108,7 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag
cy.uiOpenProductMenu('About Mattermost');
verifyEmojiPickerNotOpen();
cy.uiOpenTeamMenu('View Members');
cy.uiOpenTeamMenu('View members');
verifyEmojiPickerNotOpen();
cy.uiOpenProfileModal('Profile Settings');
@@ -167,9 +167,6 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag
// # Close the expanded RHS
cy.findByLabelText('Collapse Sidebar Icon').click();
// # Open the pinned posts
cy.uiGetChannelPinButton().click();
// # Expand RHS
cy.findByLabelText('Expand Sidebar Icon').click();

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

@@ -21,6 +21,7 @@ describe('Message Reply', () => {
newChannel = channel;
cy.visit(`/${team.name}/channels/${channel.name}`);
// HERE WE NEED TO UPDATE THE AREA SINCE WE REMOVED THIS ID
// # Wait for the page to fully load before continuing
cy.get('#sidebar-header-container').should('be.visible').and('have.text', team.display_name);
});

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

@@ -38,7 +38,7 @@ describe('Send a DM', () => {
cy.visit(testChannelUrl);
cy.get('#postListContent', {timeout: TIMEOUTS.HALF_MIN}).should('be.visible');
cy.uiGetLHSHeader().click();
cy.findByText('Leave Team').click();
cy.findByText('Leave team').click();
cy.findByText('Yes').click();
cy.url().should('include', '/select_team');

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

@@ -108,7 +108,7 @@ describe('Notifications', () => {
// # Invites User 1
cy.get('#member_popover').should('be.visible').click();
cy.contains('Manage Members').click();
cy.contains('Manage members').click();
cy.contains('Add Members').click();
cy.get('.channel-invite__content').
should('be.visible').

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

@@ -64,7 +64,7 @@ describe('Onboarding', () => {
cy.reload();
// # Open the 'Invite People' modal
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Wait half a second to ensure that the modal has been fully loaded
cy.wait(TIMEOUTS.HALF_SEC);

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

@@ -44,7 +44,7 @@ describe('Onboarding', () => {
it('MM-T400 Create account from login page link using email-password', () => {
// # Open team menu and click on "Team Settings"
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Check that the 'Team Settings' modal was opened
cy.get('#teamSettingsModal').should('exist').within(() => {

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

@@ -47,7 +47,7 @@ describe('Onboarding', () => {
stubClipboard().as('clipboard');
// # Open the 'Invite People' full screen modal and get the invite url
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Copy invite link to clipboard
cy.findByTestId('InviteView__copyInviteLink').click();

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

@@ -35,7 +35,7 @@ describe('Environment', () => {
cy.visit(townsquareLink);
// # Open team menu and click "Team Settings"
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Check that the 'Team Settings' modal was opened
cy.get('#teamSettingsModal').should('exist').within(() => {
@@ -79,7 +79,7 @@ describe('Environment', () => {
cy.visit(townsquareLink);
// # Open team menu and click "Team Settings"
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Check that the 'Team Settings' modal was opened
cy.get('#teamSettingsModal').should('exist').within(() => {
@@ -123,7 +123,7 @@ describe('Environment', () => {
cy.visit(townsquareLink);
// # Open team menu and click "Team Settings"
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Check that the 'Team Settings' modal was opened
cy.get('#teamSettingsModal').should('exist').within(() => {

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

@@ -66,7 +66,7 @@ describe('User Management', () => {
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
// # Open team menu and click View Members
cy.uiOpenTeamMenu('View Members');
cy.uiOpenTeamMenu('View members');
// * Check View Members modal dialog
cy.get('#teamMembersModal').should('be.visible').within(() => {

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

@@ -55,7 +55,7 @@ describe('Team Settings', () => {
it('MM-T385 Invite new user to closed team using email invite', () => {
// # Open 'Team Settings' modal
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Check that the 'Team Settings' modal was opened
cy.get('#teamSettingsModal').should('exist').within(() => {
@@ -75,7 +75,7 @@ describe('Team Settings', () => {
});
// # Open the 'Invite People' full screen modal
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Wait half a second to ensure that the modal has been fully loaded
cy.wait(TIMEOUTS.HALF_SEC);

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

@@ -23,7 +23,7 @@ describe('Teams Suite', () => {
it('MM-T383 Create a new team', () => {
// # Open team menu and click "Create a Team"
cy.uiOpenTeamMenu('Create a Team');
cy.uiOpenTeamMenu('Create a team');
// # Input team name as Team Test
const teamName = 'Team Test';
@@ -69,7 +69,7 @@ describe('Teams Suite', () => {
function tryReservedTeamURLAndVerifyError(teamURL) {
// # Open team menu and click "Create a Team"
cy.uiOpenTeamMenu('Create a Team');
cy.uiOpenTeamMenu('Create a team');
// # Input passed in team name
cy.get('#teamNameInput').should('be.visible').type(teamURL);

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

@@ -10,7 +10,7 @@ import {
export const allowOnlyUserFromSpecificDomain = (domain) => {
// # Open 'Team Settings' modal
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Check that the 'Team Settings' modal was opened
cy.get('#teamSettingsModal').should('exist').within(() => {
@@ -31,7 +31,7 @@ export const allowOnlyUserFromSpecificDomain = (domain) => {
export const inviteUserByEmail = (email) => {
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Wait half a second to ensure that the modal has been fully loaded
cy.wait(TIMEOUTS.HALF_SEC);

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

@@ -32,7 +32,7 @@ describe('Invite Members', () => {
cy.visit('/');
// # Open and select invite menu item
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
});
it('allows user to exit when there are no inputs', () => {

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

@@ -111,7 +111,7 @@ describe('Invite Members', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open and select invite menu item
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// * Users emails input is focused by default
cy.get('.users-emails-input__control--is-focused').should('be.visible');
@@ -181,7 +181,7 @@ function inviteUserToTeamAsMember(testUser, testTeam, user) {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open and select invite menu item
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// * Verify Invite Members
verifyInviteMembersModal(testTeam);
@@ -196,7 +196,7 @@ function inviteUserToTeamAsSysadmin(testTeam, user) {
cy.visit(`/${testTeam.name}/channels/off-topic`);
// # Open and select invite menu item
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// * Verify Invite Members
verifyInviteMembersModal(testTeam);

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

@@ -34,7 +34,7 @@ describe('Team Settings', () => {
const inviteFailedMessage = `The following email addresses do not belong to an accepted domain: ${invalidEmail}. Please contact your System Administrator for details.`;
// # Open team menu and click 'Team Settings'
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Check that the 'Team Settings' modal was opened
cy.get('#teamSettingsModal').should('exist').within(() => {
@@ -55,7 +55,7 @@ describe('Team Settings', () => {
});
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Invite user with valid email domain that is not in the team
inviteNewMemberToTeam(newUser.email);

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

@@ -42,7 +42,7 @@ describe('Team Settings', () => {
stubClipboard().as('clipboard');
// # Open team menu and click 'Team Settings'
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Check that the 'Team Settings' modal was opened
cy.get('#teamSettingsModal').should('exist').within(() => {
@@ -63,7 +63,7 @@ describe('Team Settings', () => {
cy.uiClose();
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Get the invite URL
cy.findByTestId('InviteView__copyInviteLink').should('be.visible').click();
@@ -94,7 +94,7 @@ describe('Team Settings', () => {
it('MM-T2341 Cannot add a user to a team if the user\'s email is not from the correct domain', () => {
// # Open team menu and click 'Team Settings'
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Check that the 'Team Settings' modal was opened
cy.get('#teamSettingsModal').should('exist').within(() => {
@@ -136,7 +136,7 @@ describe('Team Settings', () => {
cy.visit(`/${otherTeam.name}/channels/town-square`);
// # Open team menu and click 'Join Another Team'
cy.uiOpenTeamMenu('Join Another Team');
cy.uiOpenTeamMenu('Join another team');
// # Try to join the existing team
cy.get('.signup-team-dir').find(`#${testTeam.display_name.replace(' ', '_')}`).scrollIntoView().click();

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

@@ -34,7 +34,7 @@ describe('Manage Members', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click 'Manage Members'
cy.uiOpenTeamMenu('Manage Members');
cy.uiOpenTeamMenu('Manage members');
// # Open member dropdown
cy.get(`#teamMembersDropdown_${testUser.username}`).should('be.visible').click();
@@ -61,7 +61,7 @@ describe('Manage Members', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click 'Manage Members'
cy.uiOpenTeamMenu('Manage Members');
cy.uiOpenTeamMenu('Manage members');
// # Open member dropdown
cy.get(`#teamMembersDropdown_${user.username}`).should('be.visible').click();
@@ -94,7 +94,7 @@ describe('Manage Members', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click 'Manage Members'
cy.uiOpenTeamMenu('Manage Members');
cy.uiOpenTeamMenu('Manage members');
// # Open member dropdown
cy.get(`#teamMembersDropdown_${user.username}`).should('be.visible').click();
@@ -139,7 +139,7 @@ describe('Manage Members', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click 'Manage Members'
cy.uiOpenTeamMenu('Manage Members');
cy.uiOpenTeamMenu('Manage members');
// # Open member dropdown
cy.get(`#teamMembersDropdown_${user.username}`).should('be.visible').click();

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

@@ -61,7 +61,7 @@ describe('Teams Settings', () => {
function openTeamSettingsDialog() {
// # Open team menu and click 'Team Settings'
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Verify the team settings dialog is open
cy.get('#teamSettingsModalLabel').should('be.visible').and('contain', 'Team Settings');

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

@@ -50,7 +50,7 @@ describe('Teams Suite', () => {
it('MM-T384 Team Admin can use Next button to page through list in Manage Members', () => {
// # Open team menu and click "Manage Members"
cy.uiOpenTeamMenu('Manage Members');
cy.uiOpenTeamMenu('Manage members');
// * Check Manage Members modal dialog
cy.get('#teamMemberModalLabel').should('be.visible');

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

@@ -41,7 +41,7 @@ describe('Teams Suite', () => {
cy.url().should('include', `/${testTeam.name}/channels/town-square`);
// # Open team menu and click "Manage Members"
cy.uiOpenTeamMenu('Leave Team');
cy.uiOpenTeamMenu('Leave team');
// * Check that the "leave team modal" opened up
cy.get('#leaveTeamModal').should('be.visible');
@@ -75,8 +75,8 @@ describe('Teams Suite', () => {
// # Open team menu and click "Invite People"
cy.uiOpenTeamMenu();
cy.uiGetLHSTeamMenu().findByText('Add people to the team');
cy.uiGetLHSTeamMenu().findByText('Invite People').click().wait(TIMEOUTS.HALF_SEC);
cy.uiGetLHSTeamMenu().find('div.label-elements > span:nth-child(2)').should('have.text', 'Add or invite people to the team');
cy.uiGetLHSTeamMenu().find("li span:contains('Invite people')").should('have.text', 'Invite people').click().wait(TIMEOUTS.HALF_SEC);
// * Check that the Invitation Modal opened up
cy.findByTestId('invitationModal', {timeout: TIMEOUTS.HALF_SEC}).should('be.visible');
@@ -184,7 +184,7 @@ describe('Teams Suite', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click "Team Settings"
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// # Go to Access section
cy.get('#accessButton').click();
@@ -208,7 +208,7 @@ describe('Teams Suite', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click "Team Settings"
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// # Click on the team name menu item
cy.findByText('Team Name').should('be.visible').click();
@@ -236,7 +236,7 @@ describe('Teams Suite', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click "Team Settings"
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// # Change team description in the input
cy.get('#teamDescription').should('be.visible').clear().type(teamDescription);
@@ -246,7 +246,7 @@ describe('Teams Suite', () => {
cy.uiSaveAndClose();
// # Open team menu and click "Team Settings"
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// * Verify team description is updated
cy.get('#teamDescription').should('have.text', teamDescription);
@@ -257,7 +257,7 @@ describe('Teams Suite', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click "Team Settings"
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// # Go to Access section
cy.get('#accessButton').click();
@@ -296,7 +296,7 @@ describe('Teams Suite', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click "Team Settings"
cy.uiOpenTeamMenu('Team Settings');
cy.uiOpenTeamMenu('Team settings');
// # Go to Access section
cy.get('#accessButton').click();
@@ -314,7 +314,7 @@ describe('Teams Suite', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click "Join Another Team"
cy.uiOpenTeamMenu('Join Another Team');
cy.uiOpenTeamMenu('Join another team');
// # Verify the original test team isn't on the list
cy.get('.signup-team-dir').children().should('not.contain', `#${testTeam.name.charAt(0).toUpperCase() + testTeam.name.slice(1)}`);
@@ -329,7 +329,7 @@ describe('Teams Suite', () => {
cy.visit(`/${testTeam.name}/channels/town-square`);
// # Open team menu and click "View Members"
cy.uiOpenTeamMenu('View Members');
cy.uiOpenTeamMenu('View members');
cy.wait(TIMEOUTS.HALF_SEC);
cy.get('#searchUsersInput').should('be.visible').type('sysadmin');
cy.get('.more-modal__list').should('be.visible').children().should('have.length', 1);
@@ -341,7 +341,7 @@ function removeTeamMember(teamName, username) {
cy.visit(`/${teamName}`);
// # Open team menu and click "Manage Members"
cy.uiOpenTeamMenu('Manage Members');
cy.uiOpenTeamMenu('Manage members');
cy.get(`#teamMembersDropdown_${username}`).should('be.visible').click();
cy.get('#removeFromTeam').should('be.visible').click();

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

@@ -79,7 +79,7 @@ function getInvitePeopleLink(settings: TestSettings): ChainableT<any> {
cy.checkLeftSideBar(settings);
// # Open team menu and click 'Invite People'
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
stubClipboard().as('clipboard');
cy.checkInvitePeoplePage();

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

@@ -9,7 +9,7 @@ Cypress.Commands.add('uiGetLHS', () => {
Cypress.Commands.add('uiGetLHSHeader', () => {
return cy.uiGetLHS().
find('.SidebarHeaderMenuWrapper').
find('#sidebarTeamMenuButton').
should('be.visible');
});
@@ -35,7 +35,7 @@ Cypress.Commands.add('uiGetLHSAddChannelButton', () => {
});
Cypress.Commands.add('uiGetLHSTeamMenu', () => {
return cy.uiGetLHS().find('#sidebarDropdownMenu');
return cy.get('#sidebarTeamMenu');
});
function uiOpenSystemConsoleMenu(item = ''): ChainableT<JQuery> {

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

@@ -7,7 +7,7 @@
*/
function uiInviteMemberToCurrentTeam(username: string) {
// # Open member invite screen
cy.uiOpenTeamMenu('Invite People');
cy.uiOpenTeamMenu('Invite people');
// # Open members section if licensed for guest accounts
cy.findByTestId('invitationModal').

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

@@ -393,8 +393,8 @@ function getCurrentTeamURL(siteURL: string): ChainableT<string> {
Cypress.Commands.add('getCurrentTeamURL', getCurrentTeamURL);
function leaveTeam() {
// # Open team menu and click "Leave Team"
cy.uiOpenTeamMenu('Leave Team');
// # Open team menu and click "Leave team"
cy.uiOpenTeamMenu('Leave team');
// * Check that the "leave team modal" opened up
cy.get('#leaveTeamModal').should('be.visible');

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

@@ -5,7 +5,7 @@ import {Locator, Page} from '@playwright/test';
export {waitUntil} from 'async-wait-until';
const visibilityHidden = 'visibility: hidden !important;';
const hideTeamHeader = `.test-team-header {${visibilityHidden}} `;
const hideTeamHeader = `#sidebarTeamMenuButton {${visibilityHidden}} `;
const hidePostHeaderTime = `.post__time {${visibilityHidden}} `;
const hidePostProfileIcon = `.profile-icon {${visibilityHidden}} `;

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

@@ -12,7 +12,6 @@
"@guyplusplus/turndown-plugin-gfm": "1.0.7",
"@mattermost/client": "*",
"@mattermost/compass-components": "^0.2.12",
"@mattermost/compass-icons": "0.1.39",
"@mattermost/desktop-api": "5.10.0-2",
"@mattermost/types": "*",
"@mui/base": "5.0.0-alpha.127",

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

@@ -17,6 +17,7 @@ import MultiSelect from 'components/multiselect/multiselect';
import type {Value} from 'components/multiselect/multiselect';
import groupsAvatar from 'images/groups-avatar.png';
import {focusElement} from 'utils/a11y_utils';
import Constants from 'utils/constants';
const GROUPS_PER_PAGE = 50;
@@ -30,6 +31,7 @@ type Props = {
intl: IntlShape;
searchTerm: string;
groups: Group[];
focusOriginElement?: string;
// used in tandem with 'skipCommit' to allow using this component without performing actual linking
excludeGroups?: Group[];
@@ -114,6 +116,9 @@ export class AddGroupsToTeamModal extends React.PureComponent<Props, State> {
// public for tests
public handleExit = (): void => {
if (this.props.focusOriginElement) {
focusElement(this.props.focusOriginElement, true);
}
this.props.onExited();
};

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

@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DateTime} from 'luxon';
import React from 'react';
import type {DeepPartial} from '@mattermost/types/utilities';
@@ -22,12 +23,6 @@ function getBaseState(): DeepPartial<GlobalState> {
type: 'D',
name: 'current_user_id__teammate_user_id',
},
dm_near_timezone: {
id: 'dm_same_timezone',
teammate_id: 'teammate_near_timezone_id',
type: 'D',
name: 'current_user_id__teammate_near_timezone_id',
},
bot_dm_channel_id: {
id: 'bot_dm_channel_id',
teammate_id: 'bot_user_id',
@@ -57,18 +52,6 @@ function getBaseState(): DeepPartial<GlobalState> {
manualTimezone: '',
},
},
teammate_near_timezone_id: {
id: 'teammate_near_timezone_id',
username: 'teammate_near_timezone_username',
nickname: 'teammate_near_timezone_nickname',
first_name: 'teammate_near_timezone_first_name',
last_name: 'teammate_near_timezone_last_name',
timezone: {
useAutomaticTimezone: 'false',
automaticTimezone: '',
manualTimezone: 'CET',
},
},
bot_user_id: {
id: 'bot_user_id',
username: 'bot_username',
@@ -109,12 +92,13 @@ function getBaseState(): DeepPartial<GlobalState> {
}
describe('useTimePostBoxIndicator', () => {
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2021-01-01T18:00:00Z').getTime());
});
it('should pass base case', () => {
const fakeLocal = DateTime.local(2025, 1, 1, 3, {
zone: 'Asia/Kolkata',
});
DateTime.local = jest.fn(() => fakeLocal);
const {result: {current}} = renderHookWithContext(() => useTimePostBoxIndicator('dm_channel_id'), getBaseState());
expect(current.isDM).toBe(true);
@@ -127,20 +111,12 @@ describe('useTimePostBoxIndicator', () => {
expect(current.teammateTimezone.automaticTimezone).toBe('IST');
});
it('should not show if within working hours', () => {
const {result: {current}} = renderHookWithContext(() => useTimePostBoxIndicator('dm_near_timezone'), getBaseState());
expect(current.isDM).toBe(true);
expect(current.showDndWarning).toBe(false);
expect(current.isSelfDM).toBe(false);
expect(current.isBot).toBe(false);
expect(current.showRemoteUserHour).toBe(false);
expect(current.isScheduledPostEnabled).toBe(true);
expect(current.teammateTimezone.useAutomaticTimezone).toBe(false);
expect(current.teammateTimezone.manualTimezone).toBe('CET');
});
it('should work for DM with bots', () => {
const fakeLocal = DateTime.local(2025, 1, 1, 3, {
zone: 'Asia/Kolkata',
});
DateTime.local = jest.fn(() => fakeLocal);
const {result: {current}} = renderHookWithContext(() => useTimePostBoxIndicator('bot_dm_channel_id'), getBaseState());
expect(current.isDM).toBe(true);
@@ -155,6 +131,12 @@ describe('useTimePostBoxIndicator', () => {
});
it('should handle teammate not loaded', () => {
const fakeLocal = DateTime.local(2025, 1, 1, 1, {
zone: 'Asia/Kolkata',
});
DateTime.local = jest.fn(() => fakeLocal);
const {result: {current}} = renderHookWithContext(() => useTimePostBoxIndicator('unknown_dm_channel_id'), getBaseState());
expect(current.isDM).toBe(true);

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

@@ -14,7 +14,7 @@ import {IDENTIFIER_PATH_PATTERN, ID_PATH_PATTERN, TEAM_NAME_PATH_PATTERN} from '
import type {OwnProps, PropsFromRedux} from './index';
const ChannelHeaderMobile = makeAsyncComponent('ChannelHeaderMobile', lazy(() => import('components/channel_header_mobile')));
const MobileChannelHeader = makeAsyncComponent('MobileChannelHeader', lazy(() => import('components/mobile_channel_header')));
const GlobalThreads = makeAsyncComponent('GlobalThreads', lazy(() => import('components/threading/global_threads')),
(
<div className='app__content'>
@@ -76,15 +76,7 @@ export default class CenterChannel extends React.PureComponent<Props, State> {
'move--left-small': this.props.rhsMenuOpen,
})}
>
{isMobileView && (
<>
<div className='row header'>
<div id='navbar_wrapper'>
<ChannelHeaderMobile/>
</div>
</div>
</>
)}
{isMobileView && <MobileChannelHeader/>}
<div className='row main'>
<Switch>
<Route

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

@@ -34,6 +34,10 @@ jest.mock('mattermost-redux/selectors/entities/general', () => ({
...jest.requireActual('mattermost-redux/selectors/entities/general') as typeof import('mattermost-redux/selectors/entities/general'),
}));
jest.mock('selectors/views/browser', () => ({
getIsMobileView: () => false,
}));
describe('ChannelController', () => {
beforeEach(() => {
mockState = {

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

@@ -9,6 +9,7 @@ import {cleanUpStatusAndProfileFetchingPoll} from 'mattermost-redux/actions/stat
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
import {addVisibleUsersInCurrentChannelAndSelfToStatusPoll} from 'actions/status_actions';
import {getIsMobileView} from 'selectors/views/browser';
import {makeAsyncComponent} from 'components/async_load';
import CenterChannel from 'components/channel_layout/center_channel';
@@ -24,6 +25,7 @@ import {isInternetExplorer, isEdge} from 'utils/user_agent';
const ProductNoticesModal = makeAsyncComponent('ProductNoticesModal', lazy(() => import('components/product_notices_modal')));
const ResetStatusModal = makeAsyncComponent('ResetStatusModal', lazy(() => import('components/reset_status_modal')));
const MobileSidebarRight = makeAsyncComponent('MobileSidebarRight', lazy(() => import('components/mobile_sidebar_right')));
const BODY_CLASS_FOR_CHANNEL = ['app__body', 'channel-view'];
@@ -32,6 +34,7 @@ type Props = {
}
export default function ChannelController(props: Props) {
const isMobileView = useSelector(getIsMobileView);
const enabledUserStatuses = useSelector(getIsUserStatusesConfigEnabled);
const dispatch = useDispatch();
@@ -85,6 +88,7 @@ export default function ChannelController(props: Props) {
<ResetStatusModal/>
</div>
</div>
{isMobileView && <MobileSidebarRight/>}
</>
);
}

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

@@ -83,7 +83,7 @@ export type Props = {
initialValue?: string;
inviteAsGuest?: boolean;
roleForTrackFlow: {started_by_role: string};
focusOriginElement: string;
focusOriginElement?: string;
}
export const View = {
@@ -132,7 +132,9 @@ export default class InvitationModal extends React.PureComponent<Props, State> {
};
handleExit = () => {
focusElement(this.props.focusOriginElement, true);
if (this.props.focusOriginElement) {
focusElement(this.props.focusOriginElement, true);
}
this.props.onExited?.();
};

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -1,40 +0,0 @@
.LearnAboutTeamsLink {
>span {
width: 100%;
a {
display: flex;
align-items: center;
padding: 6px 18px;
column-gap: 10px;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
>i {
display: inline-flex !important;
padding: 3px;
border-radius: 50%;
background-color: hsla(221, 77%, 48%, 0.08);
font-size: 14px !important;
&::before {
margin: 0;
}
}
span {
font-size: 12px;
font-weight: 600;
line-height: 16px;
&:hover {
text-decoration: underline;
}
}
}
}
}

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

@@ -1,31 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import ExternalLink from 'components/external_link';
import './learn_about_teams_link.scss';
const LearnAboutTeamsLink = () => {
return (
<div className='LearnAboutTeamsLink'>
<FormattedMessage
id='learn_about_teams'
defaultMessage='<a>Learn about teams</a>'
values={{
a: (chunks) => (
<ExternalLink
location='learn_about_teams'
href='https://mattermost.com/pl/mattermost-academy-team-training'
>
<i className='icon icon-lightbulb-outline'/>
<span>{chunks}</span>
</ExternalLink>
),
}}
/>
</div>
);
};
export default LearnAboutTeamsLink;

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

@@ -1,13 +0,0 @@
.MainMenu_dropdown-link {
a {
&:focus,
&:hover,
&:active {
background-color: rgba(63, 67, 80, 0.1);
}
i + span:hover {
text-decoration: none;
}
}
}

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

@@ -1,300 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import type {ComponentProps} from 'react';
import React from 'react';
import {createIntl} from 'react-intl';
import {Provider} from 'react-redux';
import {Permissions} from 'mattermost-redux/constants';
import Menu from 'components/widgets/menu/menu';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {TestHelper} from 'utils/test_helper';
import {MainMenu} from './main_menu';
import type {Props} from './main_menu';
describe('components/Menu', () => {
// Neccessary for components enhanced by HOCs due to issue with enzyme.
// See https://github.com/enzymejs/enzyme/issues/539
const getMainMenuWrapper = (props: Props) => {
return shallow(<MainMenu {...props}/>);
// const wrapper = shallowWithIntl(<MainMenu {...props}/>);
// return wrapper.find('MainMenu').shallow();
};
const defaultProps: ComponentProps<typeof MainMenu> = {
mobile: false,
teamId: 'team-id',
teamName: 'team_name',
currentUser: TestHelper.getUserMock(),
appDownloadLink: undefined,
enableCommands: false,
enableIncomingWebhooks: false,
enableOAuthServiceProvider: false,
enableOutgoingWebhooks: false,
canManageSystemBots: false,
canManageIntegrations: true,
experimentalPrimaryTeam: undefined,
helpLink: undefined,
reportAProblemLink: undefined,
moreTeamsToJoin: false,
pluginMenuItems: [],
isMentionSearch: false,
intl: createIntl({locale: 'en', defaultLocale: 'en', timeZone: 'Etc/UTC', textComponent: 'span'}),
guestAccessEnabled: true,
canInviteTeamMember: true,
actions: {
openModal: jest.fn(),
showMentions: jest.fn(),
showFlaggedPosts: jest.fn(),
closeRightHandSide: jest.fn(),
closeRhsMenu: jest.fn(),
},
teamIsGroupConstrained: false,
isCloud: false,
isStarterFree: false,
isFreeTrial: false,
usageDeltaTeams: 1,
};
const defaultState = {
entities: {
channels: {
myMembers: {},
},
general: {
config: {},
license: {
Cloud: 'false',
},
},
teams: {
currentTeamId: 'team-id',
myMembers: {
'team-id': {
team_id: 'team-id',
user_id: 'test-user-id',
roles: 'team_user',
scheme_user: true,
},
},
},
users: {
currentUserId: 'test-user-id',
profiles: {
'test-user-id': {
id: 'test-user-id',
roles: 'system_user system_manager',
},
},
},
roles: {
roles: {
system_manager: {
permissions: [
Permissions.SYSCONSOLE_WRITE_PLUGINS,
],
},
},
},
},
};
test('should match snapshot with id', () => {
const props = {...defaultProps, id: 'test-id'};
const wrapper = getMainMenuWrapper(props);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with most of the thing disabled', () => {
const wrapper = getMainMenuWrapper(defaultProps);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with most of the thing disabled in mobile', () => {
const props = {...defaultProps, mobile: true};
const wrapper = getMainMenuWrapper(props);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with most of the thing enabled', () => {
const props = {
...defaultProps,
appDownloadLink: 'test',
enableCommands: true,
enableCustomEmoji: true,
canCreateOrDeleteCustomEmoji: true,
enableIncomingWebhooks: true,
enableOAuthServiceProvider: true,
enableOutgoingWebhooks: true,
enableUserCreation: true,
enableEmailInvitations: true,
enablePluginMarketplace: true,
experimentalPrimaryTeam: 'test',
helpLink: 'test-link-help',
reportAProblemLink: 'test-report-link',
moreTeamsToJoin: true,
};
const wrapper = getMainMenuWrapper(props);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with most of the thing enabled in mobile', () => {
const props = {
...defaultProps,
mobile: true,
appDownloadLink: 'test',
enableCommands: true,
enableCustomEmoji: true,
canCreateOrDeleteCustomEmoji: true,
enableIncomingWebhooks: true,
enableOAuthServiceProvider: true,
enableOutgoingWebhooks: true,
enableUserCreation: true,
enableEmailInvitations: true,
enablePluginMarketplace: true,
experimentalPrimaryTeam: 'test',
helpLink: 'test-link-help',
reportAProblemLink: 'test-report-link',
moreTeamsToJoin: true,
};
const wrapper = getMainMenuWrapper(props);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with plugins', () => {
const props: ComponentProps<typeof MainMenu> = {
...defaultProps,
pluginMenuItems: [{
id: 'plugin-id-1',
pluginId: 'plugin-1',
mobileIcon: <i className='fa fa-anchor'/>,
action: jest.fn,
text: 'some text',
},
{
id: 'plugind-id-2',
pluginId: 'plugin-2',
mobileIcon: <i className='fa fa-anchor'/>,
action: jest.fn,
text: 'some text',
},
],
};
const wrapper = getMainMenuWrapper(props);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with plugins in mobile', () => {
const props: ComponentProps<typeof MainMenu> = {
...defaultProps,
mobile: true,
pluginMenuItems: [{
id: 'plugin-id-1',
pluginId: 'plugin-1',
mobileIcon: <i className='fa fa-anchor'/>,
action: jest.fn,
text: 'some text',
},
{
id: 'plugind-id-2',
pluginId: 'plugin-2',
mobileIcon: <i className='fa fa-anchor'/>,
action: jest.fn,
text: 'some text',
},
],
};
const wrapper = getMainMenuWrapper(props);
expect(wrapper).toMatchSnapshot();
});
test('should show leave team option when primary team is not set', () => {
const props = {...defaultProps, teamIsGroupConstrained: false, experimentalPrimaryTeam: undefined};
const wrapper = getMainMenuWrapper(props);
// show leave team option when experimentalPrimaryTeam is not set
expect(wrapper.find('#leaveTeam')).toHaveLength(1);
expect(wrapper.find('#leaveTeam').find(Menu.ItemToggleModalRedux).props().show).toEqual(true);
});
test('should hide leave team option when experimentalPrimaryTeam is same as current team', () => {
const props = {...defaultProps, teamIsGroupConstrained: false};
const wrapper = getMainMenuWrapper(props);
expect(wrapper.find('#leaveTeam')).toHaveLength(1);
expect(wrapper.find('#leaveTeam').find(Menu.ItemToggleModalRedux).props().show).toEqual(true);
});
test('should hide leave team option when experimentalPrimaryTeam is same as current team', () => {
const props = {...defaultProps, teamIsGroupConstrained: false, experimentalPrimaryTeam: 'other-team'};
const wrapper = getMainMenuWrapper(props);
expect(wrapper.find('#leaveTeam')).toHaveLength(1);
expect(wrapper.find('#leaveTeam').find(Menu.ItemToggleModalRedux).props().show).toEqual(true);
});
test('mobile view should hide the subscribe now button when does not have permissions', () => {
const noPermissionsState = {...defaultState};
noPermissionsState.entities.roles.roles.system_manager.permissions = [];
const store = mockStore(noPermissionsState);
const wrapper = mountWithIntl(
<Provider store={store}>
<MainMenu {...defaultProps}/>
</Provider>,
);
expect(wrapper.find('UpgradeLink')).toHaveLength(0);
});
test('mobile view should hide start trial menu item because user state does not have permission to write license', () => {
const store = mockStore(defaultState);
const wrapper = mountWithIntl(
<Provider store={store}>
<MainMenu {...defaultProps}/>
</Provider>,
);
expect(wrapper.find('#startTrial')).toHaveLength(0);
});
test('should match snapshot with guest access disabled and no team invite permission', () => {
const props = {
...defaultProps,
guestAccessEnabled: false,
canInviteTeamMember: false,
};
const wrapper = getMainMenuWrapper(props);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with cloud free trial', () => {
const props = {
...defaultProps,
isCloud: true,
isStarterFree: false,
isFreeTrial: true,
usageDeltaTeams: -1,
};
const wrapper = getMainMenuWrapper(props);
expect(wrapper.find('#createTeam')).toMatchSnapshot();
});
test('should match snapshot with cloud free and team limit reached', () => {
const props = {
...defaultProps,
isCloud: true,
isStarterFree: true,
isFreeTrial: false,
usageDeltaTeams: 0,
};
const wrapper = getMainMenuWrapper(props);
expect(wrapper.find('#createTeam')).toMatchSnapshot();
});
});

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

@@ -1,538 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {injectIntl} from 'react-intl';
import type {IntlShape} from 'react-intl';
import type {UserProfile} from '@mattermost/types/users';
import {Permissions} from 'mattermost-redux/constants';
import * as GlobalActions from 'actions/global_actions';
import {trackEvent} from 'actions/telemetry_actions';
import AboutBuildModal from 'components/about_build_modal';
import AddGroupsToTeamModal from 'components/add_groups_to_team_modal';
import InvitationModal from 'components/invitation_modal';
import LeaveTeamModal from 'components/leave_team_modal';
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
import TeamPermissionGate from 'components/permissions_gates/team_permission_gate';
import TeamGroupsManageModal from 'components/team_groups_manage_modal';
import TeamMembersModal from 'components/team_members_modal';
import TeamSettingsModal from 'components/team_settings_modal';
import UserSettingsModal from 'components/user_settings/modal';
import LeaveTeamIcon from 'components/widgets/icons/leave_team_icon';
import Menu from 'components/widgets/menu/menu';
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
import {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils';
import {Constants, LicenseSkus, ModalIdentifiers, MattermostFeatures} from 'utils/constants';
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
import {makeUrlSafe} from 'utils/url';
import * as UserAgent from 'utils/user_agent';
import type {ModalData} from 'types/actions';
import type {MainMenuAction} from 'types/store/plugins';
import LearnAboutTeamsLink from './learn_about_teams_link';
import './main_menu.scss';
export type Props = {
mobile: boolean;
id?: string;
teamId?: string;
teamName?: string;
siteName?: string;
currentUser?: UserProfile;
appDownloadLink?: string;
enableCommands: boolean;
enableIncomingWebhooks: boolean;
enableOAuthServiceProvider: boolean;
enableOutgoingWebhooks: boolean;
canManageSystemBots: boolean;
canManageIntegrations: boolean;
experimentalPrimaryTeam?: string;
helpLink?: string;
reportAProblemLink?: string;
moreTeamsToJoin: boolean;
pluginMenuItems?: MainMenuAction[];
isMentionSearch?: boolean;
teamIsGroupConstrained: boolean;
isLicensedForLDAPGroups?: boolean;
intl: IntlShape;
isCloud: boolean;
isStarterFree: boolean;
isFreeTrial: boolean;
usageDeltaTeams: number;
guestAccessEnabled: boolean;
canInviteTeamMember: boolean;
actions: {
openModal: <P>(modalData: ModalData<P>) => void;
showMentions: () => void;
showFlaggedPosts: () => void;
closeRightHandSide: () => void;
closeRhsMenu: () => void;
};
};
export class MainMenu extends React.PureComponent<Props> {
static defaultProps = {
teamType: '',
mobile: false,
pluginMenuItems: [],
};
async componentDidMount(): Promise<void> {
document.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount(): void {
document.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown = (e: KeyboardEvent): void => {
if (cmdOrCtrlPressed(e) && e.shiftKey && isKeyPressed(e, Constants.KeyCodes.A)) {
e.preventDefault();
this.props.actions.openModal({modalId: ModalIdentifiers.USER_SETTINGS, dialogType: UserSettingsModal, dialogProps: {isContentProductSettings: true, focusOriginElement: 'userAccountMenuButton'}});
}
};
handleEmitUserLoggedOutEvent = (): void => {
GlobalActions.emitUserLoggedOutEvent();
};
getFlagged = (e: Event): void => {
e.preventDefault();
this.props.actions.showFlaggedPosts();
this.props.actions.closeRhsMenu();
};
searchMentions = (e: Event): void => {
e.preventDefault();
if (this.props.isMentionSearch) {
this.props.actions.closeRightHandSide();
} else {
this.props.actions.closeRhsMenu();
this.props.actions.showMentions();
}
};
render() {
const {
appDownloadLink,
currentUser,
teamIsGroupConstrained,
isLicensedForLDAPGroups,
teamId = '',
guestAccessEnabled,
canInviteTeamMember,
} = this.props;
const safeAppDownloadLink = makeUrlSafe(appDownloadLink || '');
if (!currentUser) {
return null;
}
const pluginItems = this.props.pluginMenuItems?.map((item) => (
<Menu.ItemAction
id={item.id + '_pluginmenuitem'}
key={item.id + '_pluginmenuitem'}
onClick={() => {
if (item.action) {
item.action();
}
}}
text={item.text}
icon={this.props.mobile && item.mobileIcon}
/>
));
const someIntegrationEnabled = this.props.enableIncomingWebhooks || this.props.enableOutgoingWebhooks || this.props.enableCommands || this.props.enableOAuthServiceProvider || this.props.canManageSystemBots;
const showIntegrations = !this.props.mobile && someIntegrationEnabled && this.props.canManageIntegrations;
const teamsLimitReached = this.props.isStarterFree && !this.props.isFreeTrial && this.props.usageDeltaTeams >= 0;
const createTeamRestricted = this.props.isCloud && (this.props.isFreeTrial || teamsLimitReached);
const {formatMessage} = this.props.intl;
let invitePeopleModal = null;
if (guestAccessEnabled || canInviteTeamMember) {
invitePeopleModal = (
<Menu.ItemToggleModalRedux
id='invitePeople'
modalId={ModalIdentifiers.INVITATION}
dialogType={InvitationModal}
text={formatMessage({
id: 'navbar_dropdown.invitePeople',
defaultMessage: 'Invite People',
})}
extraText={formatMessage({
id: 'navbar_dropdown.invitePeopleExtraText',
defaultMessage: 'Add people to the team',
})}
icon={this.props.mobile && <i className='fa fa-user-plus'/>}
onClick={() => trackEvent('ui', 'click_sidebar_team_dropdown_invite_people')}
dialogProps={{focusOriginElement: 'sidebarDropdownMenuButton'}}
/>
);
}
return this.props.mobile ? (
<Menu
id={this.props.id}
ariaLabel={formatMessage({id: 'navbar_dropdown.menuAriaLabel', defaultMessage: 'main menu'})}
>
<Menu.Group>
<SystemPermissionGate
permissions={[Permissions.SYSCONSOLE_WRITE_BILLING]}
>
<Menu.CloudTrial
id='menuCloudTrial'
/>
</SystemPermissionGate>
</Menu.Group>
<Menu.Group>
<SystemPermissionGate
permissions={[Permissions.SYSCONSOLE_WRITE_ABOUT_EDITION_AND_LICENSE]}
>
<Menu.StartTrial
id='startTrial'
/>
</SystemPermissionGate>
</Menu.Group>
<Menu.Group>
<Menu.ItemAction
id='recentMentions'
onClick={this.searchMentions}
icon={<i className='mentions'>{'@'}</i>}
text={formatMessage({id: 'sidebar_right_menu.recentMentions', defaultMessage: 'Recent Mentions'})}
/>
<Menu.ItemAction
id='flaggedPosts'
onClick={this.getFlagged}
icon={<i className='fa fa-bookmark'/>}
text={formatMessage({id: 'sidebar_right_menu.flagged', defaultMessage: 'Saved messages'})}
/>
</Menu.Group>
<Menu.Group>
<Menu.ItemToggleModalRedux
id='profileSettings'
modalId={ModalIdentifiers.USER_SETTINGS}
dialogType={UserSettingsModal}
dialogProps={{isContentProductSettings: false, focusOriginElement: 'userAccountMenuButton2'}}
text={formatMessage({id: 'navbar_dropdown.profileSettings', defaultMessage: 'Profile'})}
icon={<i className='fa fa-user'/>}
/>
<Menu.ItemToggleModalRedux
id='accountSettings'
modalId={ModalIdentifiers.USER_SETTINGS}
dialogType={UserSettingsModal}
dialogProps={{isContentProductSettings: true, focusOriginElement: 'userAccountMenuButton3'}}
text={formatMessage({id: 'navbar_dropdown.accountSettings', defaultMessage: 'Settings'})}
icon={<i className='fa fa-cog'/>}
/>
</Menu.Group>
<Menu.Group>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.MANAGE_TEAM]}
>
<Menu.ItemToggleModalRedux
id='addGroupsToTeam'
show={teamIsGroupConstrained && isLicensedForLDAPGroups}
modalId={ModalIdentifiers.ADD_GROUPS_TO_TEAM}
dialogType={AddGroupsToTeamModal}
text={formatMessage({id: 'navbar_dropdown.addGroupsToTeam', defaultMessage: 'Add Groups to Team'})}
icon={<i className='fa fa-user-plus'/>}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.ADD_USER_TO_TEAM, Permissions.INVITE_GUEST]}
>
{invitePeopleModal}
</TeamPermissionGate>
</Menu.Group>
<Menu.Group>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.MANAGE_TEAM]}
>
<Menu.ItemToggleModalRedux
id='teamSettings'
modalId={ModalIdentifiers.TEAM_SETTINGS}
dialogType={TeamSettingsModal}
text={formatMessage({id: 'navbar_dropdown.teamSettings', defaultMessage: 'Team Settings'})}
icon={<i className='fa fa-globe'/>}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.MANAGE_TEAM]}
>
<Menu.ItemToggleModalRedux
id='manageGroups'
show={teamIsGroupConstrained && isLicensedForLDAPGroups}
modalId={ModalIdentifiers.MANAGE_TEAM_GROUPS}
dialogProps={{
teamID: teamId,
}}
dialogType={TeamGroupsManageModal}
text={formatMessage({id: 'navbar_dropdown.manageGroups', defaultMessage: 'Manage Groups'})}
icon={<i className='fa fa-user-plus'/>}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
>
<Menu.ItemToggleModalRedux
id='manageMembers'
modalId={ModalIdentifiers.TEAM_MEMBERS}
dialogType={TeamMembersModal}
text={formatMessage({id: 'navbar_dropdown.manageMembers', defaultMessage: 'Manage Members'})}
icon={<i className='fa fa-users'/>}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
invert={true}
>
<Menu.ItemToggleModalRedux
id='viewMembers'
modalId={ModalIdentifiers.TEAM_MEMBERS}
dialogType={TeamMembersModal}
text={formatMessage({id: 'navbar_dropdown.viewMembers', defaultMessage: 'View Members'})}
icon={<i className='fa fa-users'/>}
/>
</TeamPermissionGate>
</Menu.Group>
<Menu.Group>
<SystemPermissionGate permissions={[Permissions.CREATE_TEAM]}>
<Menu.ItemLink
id='createTeam'
show={!teamsLimitReached}
to='/create_team'
text={formatMessage({id: 'navbar_dropdown.create', defaultMessage: 'Create a Team'})}
icon={<i className='fa fa-plus-square'/>}
/>
</SystemPermissionGate>
<Menu.ItemLink
id='joinTeam'
show={!this.props.experimentalPrimaryTeam && this.props.moreTeamsToJoin}
to='/select_team'
text={formatMessage({id: 'navbar_dropdown.join', defaultMessage: 'Join Another Team'})}
icon={<i className='fa fa-plus-square'/>}
/>
<Menu.ItemToggleModalRedux
id='leaveTeam'
show={!teamIsGroupConstrained && this.props.experimentalPrimaryTeam !== this.props.teamName}
modalId={ModalIdentifiers.LEAVE_TEAM}
dialogType={LeaveTeamModal}
text={formatMessage({id: 'navbar_dropdown.leave', defaultMessage: 'Leave Team'})}
icon={<LeaveTeamIcon/>}
/>
</Menu.Group>
<Menu.Group>
{pluginItems}
</Menu.Group>
<Menu.Group>
<Menu.ItemLink
id='integrations'
show={showIntegrations}
to={'/' + this.props.teamName + '/integrations'}
text={formatMessage({id: 'navbar_dropdown.integrations', defaultMessage: 'Integrations'})}
/>
</Menu.Group>
<Menu.Group>
<Menu.ItemExternalLink
id='helpLink'
show={Boolean(this.props.helpLink)}
url={this.props.helpLink}
text={formatMessage({id: 'navbar_dropdown.help', defaultMessage: 'Help'})}
icon={<i className='fa fa-question'/>}
/>
<Menu.ItemExternalLink
id='reportLink'
show={Boolean(this.props.reportAProblemLink)}
url={this.props.reportAProblemLink}
text={formatMessage({id: 'navbar_dropdown.report', defaultMessage: 'Report a Problem'})}
icon={<i className='fa fa-phone'/>}
/>
<Menu.ItemExternalLink
id='nativeAppLink'
show={this.props.appDownloadLink && !UserAgent.isMobileApp()}
url={safeAppDownloadLink}
text={formatMessage({id: 'navbar_dropdown.nativeApps', defaultMessage: 'Download Apps'})}
icon={<i className='fa fa-mobile'/>}
/>
<Menu.ItemToggleModalRedux
id='about'
modalId={ModalIdentifiers.ABOUT}
dialogType={AboutBuildModal}
text={formatMessage({id: 'navbar_dropdown.about', defaultMessage: 'About {appTitle}'}, {appTitle: this.props.siteName || 'Mattermost'})}
icon={<i className='fa fa-info'/>}
/>
</Menu.Group>
<Menu.Group>
<Menu.ItemAction
id='logout'
onClick={this.handleEmitUserLoggedOutEvent}
text={formatMessage({id: 'navbar_dropdown.logout', defaultMessage: 'Log Out'})}
icon={<i className='fa fa-sign-out'/>}
/>
</Menu.Group>
</Menu>
) : (
<Menu
id={this.props.id}
ariaLabel={formatMessage({id: 'sidebar.team_menu.menuAriaLabel', defaultMessage: 'team menu'})}
>
<Menu.Group>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.MANAGE_TEAM]}
>
<Menu.ItemToggleModalRedux
id='addGroupsToTeam'
show={teamIsGroupConstrained && isLicensedForLDAPGroups}
modalId={ModalIdentifiers.ADD_GROUPS_TO_TEAM}
dialogType={AddGroupsToTeamModal}
text={formatMessage({id: 'navbar_dropdown.addGroupsToTeam', defaultMessage: 'Add Groups to Team'})}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.ADD_USER_TO_TEAM, Permissions.INVITE_GUEST]}
>
{invitePeopleModal}
</TeamPermissionGate>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.MANAGE_TEAM]}
>
<Menu.ItemToggleModalRedux
id='teamSettings'
modalId={ModalIdentifiers.TEAM_SETTINGS}
dialogType={TeamSettingsModal}
text={formatMessage({id: 'navbar_dropdown.teamSettings', defaultMessage: 'Team Settings'})}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.MANAGE_TEAM]}
>
<Menu.ItemToggleModalRedux
id='manageGroups'
show={teamIsGroupConstrained && isLicensedForLDAPGroups}
modalId={ModalIdentifiers.MANAGE_TEAM_GROUPS}
dialogProps={{teamID: teamId}}
dialogType={TeamGroupsManageModal}
text={formatMessage({id: 'navbar_dropdown.manageGroups', defaultMessage: 'Manage Groups'})}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
>
<Menu.ItemToggleModalRedux
id='manageMembers'
modalId={ModalIdentifiers.TEAM_MEMBERS}
dialogType={TeamMembersModal}
text={formatMessage({id: 'navbar_dropdown.manageMembers', defaultMessage: 'Manage Members'})}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={teamId}
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
invert={true}
>
<Menu.ItemToggleModalRedux
id='viewMembers'
modalId={ModalIdentifiers.TEAM_MEMBERS}
dialogType={TeamMembersModal}
text={formatMessage({id: 'navbar_dropdown.viewMembers', defaultMessage: 'View Members'})}
/>
</TeamPermissionGate>
<Menu.ItemLink
id='joinTeam'
show={!this.props.experimentalPrimaryTeam && this.props.moreTeamsToJoin}
to='/select_team'
text={formatMessage({id: 'navbar_dropdown.join', defaultMessage: 'Join Another Team'})}
/>
<Menu.ItemToggleModalRedux
id='leaveTeam'
className='destructive'
show={!teamIsGroupConstrained && this.props.experimentalPrimaryTeam !== this.props.teamName}
modalId={ModalIdentifiers.LEAVE_TEAM}
dialogType={LeaveTeamModal}
text={formatMessage({id: 'navbar_dropdown.leave', defaultMessage: 'Leave Team'})}
/>
</Menu.Group>
<Menu.Group>
<SystemPermissionGate permissions={[Permissions.CREATE_TEAM]}>
<Menu.ItemLink
id='createTeam'
to='/create_team'
className={createTeamRestricted ? 'MenuItem__with-icon-tooltip' : ''}
disabled={teamsLimitReached}
text={formatMessage({id: 'navbar_dropdown.create', defaultMessage: 'Create a Team'})}
sibling={createTeamRestricted && (
<RestrictedIndicator
feature={MattermostFeatures.CREATE_MULTIPLE_TEAMS}
minimumPlanRequiredForFeature={LicenseSkus.Professional}
blocked={!this.props.isFreeTrial}
tooltipMessage={formatMessage({
id: 'navbar_dropdown.create.tooltip.cloudFreeTrial',
defaultMessage: 'During your trial you are able to create multiple teams. These teams will be archived after your trial.',
})}
titleAdminPreTrial={formatMessage({
id: 'navbar_dropdown.create.modal.titleAdminPreTrial',
defaultMessage: 'Try unlimited teams with a free trial',
})}
messageAdminPreTrial={formatMessage({
id: 'navbar_dropdown.create.modal.messageAdminPreTrial',
defaultMessage: 'Create unlimited teams with one of our paid plans. Get the full experience of Enterprise when you start a free, {trialLength} day trial.',
},
{
trialLength: FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS,
},
)}
titleAdminPostTrial={formatMessage({
id: 'navbar_dropdown.create.modal.titleAdminPostTrial',
defaultMessage: 'Upgrade to create unlimited teams',
})}
messageAdminPostTrial={formatMessage({
id: 'navbar_dropdown.create.modal.messageAdminPostTrial',
defaultMessage: 'Multiple teams allow for context-specific spaces that are more attuned to your and your teams needs. Upgrade to the Professional plan to create unlimited teams.',
})}
titleEndUser={formatMessage({
id: 'navbar_dropdown.create.modal.titleEndUser',
defaultMessage: 'Multiple teams available in paid plans',
})}
messageEndUser={formatMessage({
id: 'navbar_dropdown.create.modal.messageEndUser',
defaultMessage: 'Multiple teams allow for context-specific spaces that are more attuned to your teams needs.',
})}
/>
)}
/>
</SystemPermissionGate>
<Menu.Group>
<div className='MainMenu_dropdown-link'>
<LearnAboutTeamsLink/>
</div>
</Menu.Group>
</Menu.Group>
<Menu.Group>
{pluginItems}
</Menu.Group>
</Menu>
);
}
}
export default injectIntl(MainMenu);

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

@@ -10,6 +10,7 @@
padding: 4px 0;
background-color: var(--center-channel-bg);
box-shadow: var(--elevation-4), 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.12) inset;
margin-block-start: 4px;
}
&.AsSubMenu {
pointer-events: none;

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

@@ -24,7 +24,7 @@ import {getIsMobileView} from 'selectors/views/browser';
import type {GlobalState} from 'types/store';
import ChannelHeaderMobile from './channel_header_mobile';
import MobileChannelHeader from './mobile_channel_header';
const isCurrentChannelMuted = createSelector(
'isCurrentChannelMuted',
@@ -53,4 +53,4 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
}, dispatch),
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChannelHeaderMobile));
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileChannelHeader));

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

@@ -6,7 +6,7 @@ import React from 'react';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ChannelHeaderMobile from './channel_header_mobile';
import ChannelHeaderMobile from './mobile_channel_header';
describe('components/ChannelHeaderMobile/ChannelHeaderMobile', () => {
global.document.querySelector = jest.fn().mockReturnValue({

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

@@ -33,7 +33,7 @@ type Props = {
};
}
export default class ChannelHeaderMobile extends React.PureComponent<Props> {
export default class MobileChannelHeader extends React.PureComponent<Props> {
componentDidMount() {
document.querySelector('.inner-wrap')?.addEventListener('click', this.hideSidebars);
}

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

@@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ConnectedProps} from 'react-redux';
import {connect} from 'react-redux';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {getIsRhsMenuOpen} from 'selectors/rhs';
import type {GlobalState} from 'types/store';
import MobileSidebarRight from './mobile_sidebar_right';
function mapStateToProps(state: GlobalState) {
return {
currentUser: getCurrentUser(state),
isOpen: getIsRhsMenuOpen(state),
};
}
const connector = connect(mapStateToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
export default connect(mapStateToProps)(MobileSidebarRight);

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

@@ -4,15 +4,29 @@
.sidebar--menu {
position: absolute;
right: 0;
display: none;
overflow: auto;
width: 220px;
width: 290px;
height: 100%;
padding: 0 0 2em;
border-right: variables.$border-gray;
background: var(--sidebar-bg);
color: rgba(var(--sidebar-text-rgb), 0.8);
-webkit-overflow-scrolling: touch;
transform: translate3d(290px, 0, 0);
transition: transform 0.3s linear;
&.move--left {
transform: translate3d(0, 0, 0);
}
.mentions {
font-size: 17px;
font-weight: bold;
}
.Menu .MenuItem.MenuItem--with-icon .MenuItem__help-text {
padding-left: 0;
}
.icon {
display: inline-block;
@@ -31,53 +45,6 @@
padding-top: 5px;
}
.team__header {
@include mixins.pie-clearfix;
display: none;
padding: 0 15px;
background: var(--sidebar-header-bg);
a {
color: variables.$white;
}
.navbar-right {
margin: 16px -5px 0;
font-size: 0.85em;
.dropdown-toggle {
padding: 0 10px;
}
.dropdown-menu {
li a {
padding: 3 20px;
color: #555;
}
}
.dropdown__icon {
display: inline-block;
width: 4px;
height: 16px;
background-size: 100% 100%;
}
}
.team__name {
overflow: hidden;
max-width: 80%;
float: left;
font-size: 1.2em;
font-weight: 600;
line-height: 50px;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.nav {
> li {
> a,

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

@@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React, {memo} from 'react';
import {CSSTransition} from 'react-transition-group';
import useGetUsageDeltas from 'components/common/hooks/useGetUsageDeltas';
import MobileRightDrawerItems from './mobile_sidebar_right_items';
import type {PropsFromRedux} from './index';
import './mobile_sidebar_right.scss';
const TRANSITION_TIMEOUT = 300; // in ms
type Props = PropsFromRedux;
const MobileRightDrawer = ({
isOpen,
currentUser,
}: Props) => {
const usageDeltas = useGetUsageDeltas();
if (!currentUser) {
return null;
}
return (
<div
className={classNames('sidebar--menu', {'move--left': isOpen})}
id='sidebar-menu'
>
<div className='nav-pills__container mobile-main-menu'>
<CSSTransition
in={isOpen}
enter={true}
exit={true}
mountOnEnter={true}
unmountOnExit={true}
timeout={TRANSITION_TIMEOUT}
>
<MobileRightDrawerItems
usageDeltaTeams={usageDeltas.teams.active}
/>
</CSSTransition>
</div>
</div>
);
};
export default memo(MobileRightDrawer);

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

@@ -1,25 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ConnectedProps} from 'react-redux';
import {connect} from 'react-redux';
import {withRouter} from 'react-router-dom';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
import {Permissions} from 'mattermost-redux/constants';
import {getCloudSubscription as selectCloudSubscription, getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
import {
getConfig,
getLicense,
} from 'mattermost-redux/selectors/entities/general';
import {haveICurrentTeamPermission, haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
import {
getJoinableTeamIds,
getCurrentTeam,
} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {openModal} from 'actions/views/modals';
import {showMentions, showFlaggedPosts, closeRightHandSide, closeMenu as closeRhsMenu} from 'actions/views/rhs';
import {getRhsState} from 'selectors/rhs';
@@ -28,28 +24,18 @@ import {isCloudLicense} from 'utils/license_utils';
import type {GlobalState} from 'types/store';
import MainMenu from './main_menu';
import MobileSidebarRightItems from './mobile_sidebar_right_items';
function mapStateToProps(state: GlobalState) {
const config = getConfig(state);
const currentTeam = getCurrentTeam(state);
const currentUser = getCurrentUser(state);
const appDownloadLink = config.AppDownloadLink;
const enableCommands = config.EnableCommands === 'true';
const siteName = config.SiteName;
const enableIncomingWebhooks = config.EnableIncomingWebhooks === 'true';
const enableOAuthServiceProvider = config.EnableOAuthServiceProvider === 'true';
const enableOutgoingWebhooks = config.EnableOutgoingWebhooks === 'true';
const experimentalPrimaryTeam = config.ExperimentalPrimaryTeam;
const helpLink = config.HelpLink;
const reportAProblemLink = config.ReportAProblemLink;
const canManageTeamIntegrations = (haveICurrentTeamPermission(state, Permissions.MANAGE_SLASH_COMMANDS) || haveICurrentTeamPermission(state, Permissions.MANAGE_OAUTH) || haveICurrentTeamPermission(state, Permissions.MANAGE_INCOMING_WEBHOOKS) || haveICurrentTeamPermission(state, Permissions.MANAGE_OUTGOING_WEBHOOKS));
const canManageSystemBots = (haveISystemPermission(state, {permission: Permissions.MANAGE_BOTS}) || haveISystemPermission(state, {permission: Permissions.MANAGE_OTHERS_BOTS}));
const canManageIntegrations = canManageTeamIntegrations || canManageSystemBots;
const canInviteTeamMember = haveICurrentTeamPermission(state, Permissions.ADD_USER_TO_TEAM);
const joinableTeams = getJoinableTeamIds(state);
const moreTeamsToJoin = joinableTeams && joinableTeams.length > 0;
const rhsState = getRhsState(state);
@@ -64,12 +50,6 @@ function mapStateToProps(state: GlobalState) {
return {
appDownloadLink,
enableCommands,
canManageIntegrations,
enableIncomingWebhooks,
enableOAuthServiceProvider,
enableOutgoingWebhooks,
canManageSystemBots,
experimentalPrimaryTeam,
helpLink,
reportAProblemLink,
@@ -78,13 +58,10 @@ function mapStateToProps(state: GlobalState) {
siteName,
teamId: currentTeam?.id,
teamName: currentTeam?.name,
currentUser,
isMentionSearch: rhsState === RHSStates.MENTION,
teamIsGroupConstrained: Boolean(currentTeam?.group_constrained),
isLicensedForLDAPGroups: state.entities.general.license.LDAPGroups === 'true',
guestAccessEnabled: config.EnableGuestAccounts === 'true',
canInviteTeamMember,
isCloud,
isStarterFree,
isFreeTrial,
};
@@ -93,7 +70,6 @@ function mapStateToProps(state: GlobalState) {
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators({
openModal,
showMentions,
showFlaggedPosts,
closeRightHandSide,
@@ -102,4 +78,8 @@ function mapDispatchToProps(dispatch: Dispatch) {
};
}
export default withRouter<any, any>(connect(mapStateToProps, mapDispatchToProps)(MainMenu));
const connector = connect(mapStateToProps, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
export default connector(MobileSidebarRightItems);

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

@@ -0,0 +1,164 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Permissions} from 'mattermost-redux/constants';
import type {MockIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {MobileSidebarRightItems} from './mobile_sidebar_right_items';
import type {Props} from './mobile_sidebar_right_items';
describe('MobileSidebarRightItems', () => {
const defaultProps: Props = {
teamId: 'team-id',
teamName: 'team_name',
appDownloadLink: undefined,
experimentalPrimaryTeam: undefined,
helpLink: undefined,
reportAProblemLink: undefined,
moreTeamsToJoin: false,
pluginMenuItems: [],
isMentionSearch: false,
usageDeltaTeams: 0,
siteName: 'site-name',
isLicensedForLDAPGroups: false,
guestAccessEnabled: true,
actions: {
showMentions: jest.fn(),
showFlaggedPosts: jest.fn(),
closeRightHandSide: jest.fn(),
closeRhsMenu: jest.fn(),
},
teamIsGroupConstrained: false,
isStarterFree: false,
isFreeTrial: false,
intl: {
formatMessage: ({defaultMessage}) => defaultMessage,
} as MockIntl,
};
const defaultState = {
entities: {
channels: {
myMembers: {},
},
general: {
config: {},
license: {
Cloud: 'false',
},
},
teams: {
currentTeamId: 'team-id',
myMembers: {
'team-id': {
team_id: 'team-id',
user_id: 'test-user-id',
roles: 'team_user',
scheme_user: true,
},
},
},
users: {
currentUserId: 'test-user-id',
profiles: {
'test-user-id': {
id: 'test-user-id',
roles: 'system_user system_admin',
},
},
},
roles: {
roles: {
system_admin: {
permissions: [
Permissions.CREATE_TEAM,
Permissions.SYSCONSOLE_WRITE_PLUGINS,
],
},
},
},
},
};
test('should render basic menu items', () => {
renderWithContext(<MobileSidebarRightItems {...defaultProps}/>, defaultState);
expect(screen.getByText('Recent Mentions')).toBeInTheDocument();
expect(screen.getByText('Saved messages')).toBeInTheDocument();
expect(screen.getByText('Profile')).toBeInTheDocument();
expect(screen.getByText('Settings')).toBeInTheDocument();
});
test('should show leave team option when primary team is not set', () => {
renderWithContext(
<MobileSidebarRightItems
{...defaultProps}
teamIsGroupConstrained={false}
experimentalPrimaryTeam={undefined}
/>,
defaultState,
);
expect(screen.getByText('Leave Team')).toBeInTheDocument();
});
test('should hide leave team option when team is group constrained', () => {
renderWithContext(
<MobileSidebarRightItems
{...defaultProps}
teamIsGroupConstrained={true}
/>,
defaultState,
);
expect(screen.queryByText('Leave Team')).not.toBeInTheDocument();
});
test('should show create team option with proper permissions', () => {
renderWithContext(<MobileSidebarRightItems {...defaultProps}/>, defaultState);
expect(screen.getByText('Create a Team')).toBeInTheDocument();
});
test('should show plugins when provided', () => {
const pluginMenuItems = [
{
id: 'plugin-1',
pluginId: 'plugin-1',
mobileIcon: <i className='fa fa-anchor'/>,
action: jest.fn(),
text: 'Plugin Item 1',
},
];
renderWithContext(
<MobileSidebarRightItems
{...defaultProps}
pluginMenuItems={pluginMenuItems}
/>,
defaultState,
);
expect(screen.getByText('Plugin Item 1')).toBeInTheDocument();
});
test('should show help link when provided', () => {
renderWithContext(
<MobileSidebarRightItems
{...defaultProps}
helpLink='https://help.example.com'
/>,
defaultState,
);
expect(screen.getByText('Help')).toBeInTheDocument();
});
test('should show report link when provided', () => {
renderWithContext(
<MobileSidebarRightItems
{...defaultProps}
reportAProblemLink='https://report.example.com'
/>,
defaultState,
);
expect(screen.getByText('Report a Problem')).toBeInTheDocument();
});
});

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

@@ -0,0 +1,299 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {injectIntl} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';
import {Permissions} from 'mattermost-redux/constants';
import {emitUserLoggedOutEvent} from 'actions/global_actions';
import {trackEvent} from 'actions/telemetry_actions';
import AboutBuildModal from 'components/about_build_modal';
import AddGroupsToTeamModal from 'components/add_groups_to_team_modal';
import InvitationModal from 'components/invitation_modal';
import LeaveTeamModal from 'components/leave_team_modal';
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
import TeamPermissionGate from 'components/permissions_gates/team_permission_gate';
import TeamGroupsManageModal from 'components/team_groups_manage_modal';
import TeamMembersModal from 'components/team_members_modal';
import TeamSettingsModal from 'components/team_settings_modal';
import UserSettingsModal from 'components/user_settings/modal';
import LeaveTeamIcon from 'components/widgets/icons/leave_team_icon';
import Menu from 'components/widgets/menu/menu';
import {ModalIdentifiers} from 'utils/constants';
import {makeUrlSafe} from 'utils/url';
import type {PropsFromRedux} from './index';
export interface Props extends PropsFromRedux, WrappedComponentProps {
usageDeltaTeams: number;
}
export class MobileSidebarRightItems extends React.PureComponent<Props> {
static defaultProps = {
pluginMenuItems: [],
};
onRecentMentionItemClick = (e: Event): void => {
e.preventDefault();
if (this.props.isMentionSearch) {
this.props.actions.closeRightHandSide();
} else {
this.props.actions.closeRhsMenu();
this.props.actions.showMentions();
}
};
onShowFlaggedPostItemClick = (e: Event): void => {
e.preventDefault();
this.props.actions.showFlaggedPosts();
this.props.actions.closeRhsMenu();
};
onLogoutItemClick = (): void => {
emitUserLoggedOutEvent();
};
render() {
const {formatMessage} = this.props.intl;
const safeAppDownloadLink = makeUrlSafe(this.props.appDownloadLink || '');
const teamsLimitReached = this.props.isStarterFree && !this.props.isFreeTrial && this.props.usageDeltaTeams >= 0;
const pluginItems = this.props.pluginMenuItems.map((item) => (
<Menu.ItemAction
id={item.id + '_pluginmenuitem'}
key={item.id + '_pluginmenuitem'}
onClick={() => {
if (item.action) {
item.action();
}
}}
text={item.text}
icon={item.mobileIcon}
/>
));
return (
<Menu
ariaLabel={formatMessage({id: 'navbar_dropdown.menuAriaLabel', defaultMessage: 'main menu'})}
>
<Menu.Group>
<SystemPermissionGate
permissions={[Permissions.SYSCONSOLE_WRITE_BILLING]}
>
<Menu.CloudTrial
id='menuCloudTrial'
/>
</SystemPermissionGate>
</Menu.Group>
<Menu.Group>
<SystemPermissionGate
permissions={[Permissions.SYSCONSOLE_WRITE_ABOUT_EDITION_AND_LICENSE]}
>
<Menu.StartTrial
id='startTrial'
/>
</SystemPermissionGate>
</Menu.Group>
<Menu.Group>
<Menu.ItemAction
id='recentMentions'
onClick={this.onRecentMentionItemClick}
icon={<i className='mentions'>{'@'}</i>}
text={formatMessage({id: 'sidebar_right_menu.recentMentions', defaultMessage: 'Recent Mentions'})}
/>
<Menu.ItemAction
id='flaggedPosts'
onClick={this.onShowFlaggedPostItemClick}
icon={<i className='fa fa-bookmark'/>}
text={formatMessage({id: 'sidebar_right_menu.flagged', defaultMessage: 'Saved messages'})}
/>
</Menu.Group>
<Menu.Group>
<Menu.ItemToggleModalRedux
id='profileSettings'
modalId={ModalIdentifiers.USER_SETTINGS}
dialogType={UserSettingsModal}
dialogProps={{isContentProductSettings: false}}
text={formatMessage({id: 'navbar_dropdown.profileSettings', defaultMessage: 'Profile'})}
icon={<i className='fa fa-user'/>}
/>
<Menu.ItemToggleModalRedux
id='accountSettings'
modalId={ModalIdentifiers.USER_SETTINGS}
dialogType={UserSettingsModal}
dialogProps={{isContentProductSettings: true}}
text={formatMessage({id: 'navbar_dropdown.accountSettings', defaultMessage: 'Settings'})}
icon={<i className='fa fa-cog'/>}
/>
</Menu.Group>
<Menu.Group>
<TeamPermissionGate
teamId={this.props.teamId}
permissions={[Permissions.MANAGE_TEAM]}
>
<Menu.ItemToggleModalRedux
id='addGroupsToTeam'
show={this.props.teamIsGroupConstrained && this.props.isLicensedForLDAPGroups}
modalId={ModalIdentifiers.ADD_GROUPS_TO_TEAM}
dialogType={AddGroupsToTeamModal}
text={formatMessage({id: 'navbar_dropdown.addGroupsToTeam', defaultMessage: 'Add Groups to Team'})}
icon={<i className='fa fa-user-plus'/>}
/>
</TeamPermissionGate>
{this.props.guestAccessEnabled && (
<TeamPermissionGate
teamId={this.props.teamId}
permissions={[Permissions.ADD_USER_TO_TEAM, Permissions.INVITE_GUEST]}
>
<Menu.ItemToggleModalRedux
id='invitePeople'
modalId={ModalIdentifiers.INVITATION}
dialogType={InvitationModal}
text={formatMessage({
id: 'navbar_dropdown.invitePeople',
defaultMessage: 'Invite People',
})}
extraText={formatMessage({
id: 'navbar_dropdown.invitePeopleExtraText',
defaultMessage: 'Add people to the team',
})}
icon={<i className='fa fa-user-plus'/>}
onClick={() => trackEvent('ui', 'click_sidebar_team_dropdown_invite_people')}
/>
</TeamPermissionGate>
)}
</Menu.Group>
<Menu.Group>
<TeamPermissionGate
teamId={this.props.teamId}
permissions={[Permissions.MANAGE_TEAM]}
>
<Menu.ItemToggleModalRedux
id='teamSettings'
modalId={ModalIdentifiers.TEAM_SETTINGS}
dialogType={TeamSettingsModal}
text={formatMessage({id: 'navbar_dropdown.teamSettings', defaultMessage: 'Team Settings'})}
icon={<i className='fa fa-globe'/>}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={this.props.teamId}
permissions={[Permissions.MANAGE_TEAM]}
>
<Menu.ItemToggleModalRedux
id='manageGroups'
show={this.props.teamIsGroupConstrained && this.props.isLicensedForLDAPGroups}
modalId={ModalIdentifiers.MANAGE_TEAM_GROUPS}
dialogProps={{
teamID: this.props.teamId,
}}
dialogType={TeamGroupsManageModal}
text={formatMessage({id: 'navbar_dropdown.manageGroups', defaultMessage: 'Manage Groups'})}
icon={<i className='fa fa-user-plus'/>}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={this.props.teamId}
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
>
<Menu.ItemToggleModalRedux
id='manageMembers'
modalId={ModalIdentifiers.TEAM_MEMBERS}
dialogType={TeamMembersModal}
text={formatMessage({id: 'navbar_dropdown.manageMembers', defaultMessage: 'Manage Members'})}
icon={<i className='fa fa-users'/>}
/>
</TeamPermissionGate>
<TeamPermissionGate
teamId={this.props.teamId}
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
invert={true}
>
<Menu.ItemToggleModalRedux
id='viewMembers'
modalId={ModalIdentifiers.TEAM_MEMBERS}
dialogType={TeamMembersModal}
text={formatMessage({id: 'navbar_dropdown.viewMembers', defaultMessage: 'View Members'})}
icon={<i className='fa fa-users'/>}
/>
</TeamPermissionGate>
</Menu.Group>
<Menu.Group>
<SystemPermissionGate permissions={[Permissions.CREATE_TEAM]}>
<Menu.ItemLink
id='createTeam'
show={!teamsLimitReached}
to='/create_team'
text={formatMessage({id: 'navbar_dropdown.create', defaultMessage: 'Create a Team'})}
icon={<i className='fa fa-plus-square'/>}
/>
</SystemPermissionGate>
<Menu.ItemLink
id='joinTeam'
show={!this.props.experimentalPrimaryTeam && this.props.moreTeamsToJoin}
to='/select_team'
text={formatMessage({id: 'navbar_dropdown.join', defaultMessage: 'Join Another Team'})}
icon={<i className='fa fa-plus-square'/>}
/>
<Menu.ItemToggleModalRedux
id='leaveTeam'
show={!this.props.teamIsGroupConstrained && this.props.experimentalPrimaryTeam !== this.props.teamName}
modalId={ModalIdentifiers.LEAVE_TEAM}
dialogType={LeaveTeamModal}
text={formatMessage({id: 'navbar_dropdown.leave', defaultMessage: 'Leave Team'})}
icon={<LeaveTeamIcon/>}
/>
</Menu.Group>
<Menu.Group>
{pluginItems}
</Menu.Group>
<Menu.Group>
<Menu.ItemExternalLink
id='helpLink'
show={Boolean(this.props.helpLink)}
url={this.props.helpLink}
text={formatMessage({id: 'navbar_dropdown.help', defaultMessage: 'Help'})}
icon={<i className='fa fa-question'/>}
/>
<Menu.ItemExternalLink
id='reportLink'
show={Boolean(this.props.reportAProblemLink)}
url={this.props.reportAProblemLink}
text={formatMessage({id: 'navbar_dropdown.report', defaultMessage: 'Report a Problem'})}
icon={<i className='fa fa-phone'/>}
/>
<Menu.ItemExternalLink
id='nativeAppLink'
show={this.props.appDownloadLink}
url={safeAppDownloadLink}
text={formatMessage({id: 'navbar_dropdown.nativeApps', defaultMessage: 'Download Apps'})}
icon={<i className='fa fa-mobile'/>}
/>
<Menu.ItemToggleModalRedux
id='about'
modalId={ModalIdentifiers.ABOUT}
dialogType={AboutBuildModal}
text={formatMessage({id: 'navbar_dropdown.about', defaultMessage: 'About {appTitle}'}, {appTitle: this.props.siteName || 'Mattermost'})}
icon={<i className='fa fa-info'/>}
/>
</Menu.Group>
<Menu.Group>
<Menu.ItemAction
id='logout'
onClick={this.onLogoutItemClick}
text={formatMessage({id: 'navbar_dropdown.logout', defaultMessage: 'Log Out'})}
icon={<i className='fa fa-sign-out'/>}
/>
</Menu.Group>
</Menu>
);
}
}
export default injectIntl(MobileSidebarRightItems);

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

@@ -164,7 +164,7 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
if (this.exitToChannel) {
getHistory().push(this.exitToChannel);
} else {
} else if (this.props.focusOriginElement) {
setTimeout(() => {
focusElement(this.props.focusOriginElement, true);
}, 0);

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

@@ -10,10 +10,12 @@ import {TourTip, useMeasurePunchouts} from '@mattermost/components';
import {setShowOnboardingCompleteProfileTour} from 'actions/views/onboarding_tasks';
import {isShowOnboardingCompleteProfileTour} from 'selectors/views/onboarding_tasks';
import {ELEMENT_ID_FOR_USER_ACCOUNT_MENU} from 'components/user_account_menu/user_account_menu';
import {OnboardingTasksName, TaskNameMapToSteps} from './constants';
import {useHandleOnBoardingTaskData} from './onboarding_tasks_manager';
const translate = {x: 1, y: -9};
const translate = {x: 1.5, y: -9};
export const CompleteYourProfileTour = () => {
const dispatch = useDispatch();
@@ -43,7 +45,7 @@ export const CompleteYourProfileTour = () => {
</p>
);
const overlayPunchOut = useMeasurePunchouts(['userAccountMenu'], [], {x: -2.5, y: -2.5, width: 5, height: 5});
const overlayPunchOut = useMeasurePunchouts([ELEMENT_ID_FOR_USER_ACCOUNT_MENU], [], {x: -2.5, y: -2.5, width: 5, height: 5});
const onDismiss = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();

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

@@ -26,7 +26,6 @@ import LoggedIn from 'components/logged_in';
import LoggedInRoute from 'components/logged_in_route';
import {LAUNCHING_WORKSPACE_FULLSCREEN_Z_INDEX} from 'components/preparing_workspace/launching_workspace';
import {Animations} from 'components/preparing_workspace/steps';
import SidebarMobileRightMenu from 'components/sidebar_mobile_right_menu';
import webSocketClient from 'client/web_websocket_client';
import {initializePlugins} from 'plugins';
@@ -584,7 +583,6 @@ export default class Root extends React.PureComponent<Props, State> {
</div>
<Pluggable pluggableName='Global'/>
<AppBar/>
<SidebarMobileRightMenu/>
</CompassThemeProvider>
</Switch>
</RootProvider>

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

@@ -33,7 +33,20 @@ exports[`components/select_team/SelectTeam should match snapshot 1`] = `
id="signup_team.join_open"
/>
</h4>
<LearnAboutTeamsLink />
<ForwardRef
className="LearnAboutTeamsLink"
href="https://mattermost.com/pl/mattermost-academy-team-training"
location="learn_about_teams"
>
<i
aria-hidden={true}
className="icon icon-lightbulb-outline"
/>
<MemoizedFormattedMessage
defaultMessage="Learn about teams"
id="learn_about_teams"
/>
</ForwardRef>
</div>
<InfiniteScroll
bufferValue={280}
@@ -150,7 +163,20 @@ exports[`components/select_team/SelectTeam should match snapshot, on create team
id="signup_team.join_open"
/>
</h4>
<LearnAboutTeamsLink />
<ForwardRef
className="LearnAboutTeamsLink"
href="https://mattermost.com/pl/mattermost-academy-team-training"
location="learn_about_teams"
>
<i
aria-hidden={true}
className="icon icon-lightbulb-outline"
/>
<MemoizedFormattedMessage
defaultMessage="Learn about teams"
id="learn_about_teams"
/>
</ForwardRef>
</div>
<InfiniteScroll
bufferValue={280}
@@ -404,7 +430,20 @@ exports[`components/select_team/SelectTeam should match snapshot, on no joinable
id="signup_team.join_open"
/>
</h4>
<LearnAboutTeamsLink />
<ForwardRef
className="LearnAboutTeamsLink"
href="https://mattermost.com/pl/mattermost-academy-team-training"
location="learn_about_teams"
>
<i
aria-hidden={true}
className="icon icon-lightbulb-outline"
/>
<MemoizedFormattedMessage
defaultMessage="Learn about teams"
id="learn_about_teams"
/>
</ForwardRef>
</div>
<InfiniteScroll
bufferValue={280}
@@ -633,7 +672,20 @@ exports[`components/select_team/SelectTeam should match snapshot, on no joinable
id="signup_team.join_open"
/>
</h4>
<LearnAboutTeamsLink />
<ForwardRef
className="LearnAboutTeamsLink"
href="https://mattermost.com/pl/mattermost-academy-team-training"
location="learn_about_teams"
>
<i
aria-hidden={true}
className="icon icon-lightbulb-outline"
/>
<MemoizedFormattedMessage
defaultMessage="Learn about teams"
id="learn_about_teams"
/>
</ForwardRef>
</div>
<InfiniteScroll
bufferValue={280}

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

@@ -5,7 +5,7 @@
margin-top: 10px;
margin-bottom: 24px;
>h4 {
> h4 {
margin: 0;
span {
@@ -15,4 +15,39 @@
line-height: 28px;
}
}
a.LearnAboutTeamsLink {
display: flex;
align-items: center;
padding: 6px 18px;
column-gap: 10px;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
> i {
display: inline-flex;
padding: 3px;
border-radius: 50%;
background-color: rgba(var(--denim-button-bg-rgb), 0.08);
font-size: 14px;
&::before {
margin: 0;
}
}
span {
font-size: 12px;
font-weight: 600;
line-height: 16px;
&:hover {
text-decoration: underline;
}
}
}
}

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

@@ -18,8 +18,8 @@ import AnnouncementBar from 'components/announcement_bar';
import BackButton from 'components/common/back_button';
import InfiniteScroll from 'components/common/infinite_scroll';
import SiteNameAndDescription from 'components/common/site_name_and_description';
import ExternalLink from 'components/external_link';
import LoadingScreen from 'components/loading_screen';
import LearnAboutTeamsLink from 'components/main_menu/learn_about_teams_link';
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
import LogoutIcon from 'components/widgets/icons/fa_logout_icon';
@@ -28,10 +28,12 @@ import Constants from 'utils/constants';
import * as UserAgent from 'utils/user_agent';
import SelectTeamItem from './components/select_team_item';
import './select_team.scss';
export const TEAMS_PER_PAGE = 30;
const TEAM_MEMBERSHIP_DENIAL_ERROR_ID = 'api.team.add_members.user_denied';
const MATTERMOST_ACADEMY_TEAM_TRAINING_LINK = 'https://mattermost.com/pl/mattermost-academy-team-training';
type Actions = {
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean) => any;
@@ -289,7 +291,20 @@ export default class SelectTeam extends React.PureComponent<Props, State> {
defaultMessage='Teams you can join: '
/>
</h4>
<LearnAboutTeamsLink/>
<ExternalLink
location='learn_about_teams'
href={MATTERMOST_ACADEMY_TEAM_TRAINING_LINK}
className='LearnAboutTeamsLink'
>
<i
className='icon icon-lightbulb-outline'
aria-hidden={true}
/>
<FormattedMessage
id='learn_about_teams'
defaultMessage='Learn about teams'
/>
</ExternalLink>
</div>
<InfiniteScroll
callBack={this.fetchMoreTeams}

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

@@ -1,134 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/sidebar should match empty div snapshot when teamId is missing 1`] = `<div />`;
exports[`components/sidebar should match snapshot 1`] = `
<ResizableLhs
className=""
id="SidebarContainer"
>
<SidebarHeader
canCreateChannel={true}
canCreateCustomGroups={true}
canJoinPublicChannel={true}
handleOpenDirectMessagesModal={[Function]}
invitePeopleModal={[Function]}
showCreateCategoryModal={[Function]}
showCreateUserGroupModal={[Function]}
showMoreChannelsModal={[Function]}
showNewChannelModal={[Function]}
unreadFilterEnabled={false}
/>
<div
aria-label="channel navigator region"
className="a11y__region"
data-a11y-sort-order="6"
id="lhsNavigator"
role="application"
>
<Connect(injectIntl(ChannelNavigator)) />
</div>
<div
className="sidebar--left__icons"
>
<Pluggable
pluggableName="LeftSidebarHeader"
/>
</div>
<Connect(injectIntl(SidebarList))
handleOpenMoreDirectChannelsModal={[Function]}
onDragEnd={[Function]}
onDragStart={[Function]}
/>
<Connect(DataPrefetch) />
</ResizableLhs>
`;
exports[`components/sidebar should match snapshot when direct channels modal is open 1`] = `
<ResizableLhs
className=""
id="SidebarContainer"
>
<SidebarHeader
canCreateChannel={true}
canCreateCustomGroups={true}
canJoinPublicChannel={true}
handleOpenDirectMessagesModal={[Function]}
invitePeopleModal={[Function]}
showCreateCategoryModal={[Function]}
showCreateUserGroupModal={[Function]}
showMoreChannelsModal={[Function]}
showNewChannelModal={[Function]}
unreadFilterEnabled={false}
/>
<div
aria-label="channel navigator region"
className="a11y__region"
data-a11y-sort-order="6"
id="lhsNavigator"
role="application"
>
<Connect(injectIntl(ChannelNavigator)) />
</div>
<div
className="sidebar--left__icons"
>
<Pluggable
pluggableName="LeftSidebarHeader"
/>
</div>
<Connect(injectIntl(SidebarList))
handleOpenMoreDirectChannelsModal={[Function]}
onDragEnd={[Function]}
onDragStart={[Function]}
/>
<Connect(DataPrefetch) />
<MoreDirectChannels
focusOriginElement="newDirectMessageButton"
isExistingChannel={false}
onModalDismissed={[Function]}
/>
</ResizableLhs>
`;
exports[`components/sidebar should match snapshot when more channels modal is open 1`] = `
<ResizableLhs
className=""
id="SidebarContainer"
>
<SidebarHeader
canCreateChannel={true}
canCreateCustomGroups={true}
canJoinPublicChannel={true}
handleOpenDirectMessagesModal={[Function]}
invitePeopleModal={[Function]}
showCreateCategoryModal={[Function]}
showCreateUserGroupModal={[Function]}
showMoreChannelsModal={[Function]}
showNewChannelModal={[Function]}
unreadFilterEnabled={false}
/>
<div
aria-label="channel navigator region"
className="a11y__region"
data-a11y-sort-order="6"
id="lhsNavigator"
role="application"
>
<Connect(injectIntl(ChannelNavigator)) />
</div>
<div
className="sidebar--left__icons"
>
<Pluggable
pluggableName="LeftSidebarHeader"
/>
</div>
<Connect(injectIntl(SidebarList))
handleOpenMoreDirectChannelsModal={[Function]}
onDragEnd={[Function]}
onDragStart={[Function]}
/>
<Connect(DataPrefetch) />
</ResizableLhs>
`;

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

@@ -1,113 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import styled from 'styled-components';
import type {UserProfile} from '@mattermost/types/users';
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
import CustomStatusModal from 'components/custom_status/custom_status_modal';
import WithTooltip from 'components/with_tooltip';
import {ModalIdentifiers} from 'utils/constants';
import type {ModalData} from 'types/actions';
const HeaderLine = styled.div`
display: flex;
padding: 2px 16px 0 0;
flex-grow: 1;
user-select: none;
color: var(--sidebar-header-text-color);
`;
const VerticalStack = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
`;
type Props = {
teamDescription?: string;
teamId?: string;
currentUser: UserProfile;
teamDisplayName?: string;
actions: Actions;
};
type Actions = {
openModal: <P>(modalData: ModalData<P>) => void;
};
export default class Contents extends React.PureComponent<Props> {
handleCustomStatusEmojiClick = (event: React.MouseEvent) => {
event.stopPropagation();
const customStatusInputModalData = {
modalId: ModalIdentifiers.CUSTOM_STATUS,
dialogType: CustomStatusModal,
};
this.props.actions.openModal(customStatusInputModalData);
};
render() {
if (!this.props.currentUser || !this.props.teamId) {
return null;
}
let teamNameWithToolTip = (
<h1
id='headerTeamName'
className='team__name'
data-teamid={this.props.teamId}
>
{this.props.teamDisplayName}
</h1>
);
if (this.props.teamDescription) {
teamNameWithToolTip = (
<WithTooltip
title={this.props.teamDescription}
>
{teamNameWithToolTip}
</WithTooltip>
);
}
return (
<div
className='SidebarHeaderDropdownButton'
id='sidebarHeaderDropdownButton'
>
<HeaderLine
id='headerInfo'
className='header__info'
>
<VerticalStack>
{teamNameWithToolTip}
<div
id='headerInfoContent'
className='header__info__content'
>
<div
id='headerUsername'
className='user__name'
>
{'@' + this.props.currentUser.username}
</div>
<CustomStatusEmoji
showTooltip={true}
emojiStyle={{
verticalAlign: 'top',
marginLeft: 2,
}}
onClick={this.handleCustomStatusEmojiClick as unknown as () => void}
/>
</div>
</VerticalStack>
</HeaderLine>
</div>
);
}
}

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

@@ -1,37 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {openModal} from 'actions/views/modals';
import type {GlobalState} from 'types/store';
import Contents from './contents';
function mapStateToProps(state: GlobalState) {
const currentTeam = getCurrentTeam(state);
const currentUser = getCurrentUser(state);
return {
currentUser,
teamDescription: currentTeam?.description,
teamDisplayName: currentTeam?.display_name,
teamId: currentTeam?.id,
};
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators({
openModal,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Contents);

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

@@ -1,5 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ConnectedProps} from 'react-redux';
import {connect} from 'react-redux';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {openModal} from 'actions/views/modals';
import type {GlobalState} from 'types/store';
import MobileSidebarHeader from './mobile_sidebar_header';
export default MobileSidebarHeader;
function mapStateToProps(state: GlobalState) {
const currentTeam = getCurrentTeam(state);
const currentUser = getCurrentUser(state);
return {
username: currentUser?.username,
teamDisplayName: currentTeam?.display_name ?? '',
};
}
const mapDispatchToProps = {
openModal,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
export default connector(MobileSidebarHeader);

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

@@ -0,0 +1,35 @@
.mobileSidebarHeader {
display: flex;
flex-direction: column;
padding: 15px;
gap: 4px;
h1 {
overflow: hidden;
margin: 0;
color: var(--sidebar-header-text-color);
font-family: Metropolis, sans-serif;
font-size: 16px;
font-weight: 600;
line-height: 22px;
text-overflow: ellipsis;
white-space: nowrap;
}
&__username {
display: flex;
overflow: hidden;
flex-direction: row;
color: rgba(var(--sidebar-header-text-color-rgb), 0.75);
font-family: "Open Sans", sans-serif;
font-size: 14px;
gap: 2px;
text-overflow: ellipsis;
white-space: nowrap;
span:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
}
}

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

@@ -2,27 +2,45 @@
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import type {MouseEvent, KeyboardEvent} from 'react';
import Contents from './contents';
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
import CustomStatusModal from 'components/custom_status/custom_status_modal';
export default function MobileSidebarHeader() {
const intl = useIntl();
const ariaLabel = intl.formatMessage({id: 'accessibility.sections.lhsHeader', defaultMessage: 'team menu region'});
import {ModalIdentifiers} from 'utils/constants';
import type {PropsFromRedux} from './index';
import './mobile_sidebar_header.scss';
type Props = PropsFromRedux;
export default function MobileSidebarHeader(props: Props) {
if (!props.username) {
return null;
}
function handleCustomStatusEmojiClick(event: MouseEvent<HTMLSpanElement> | KeyboardEvent<HTMLSpanElement>) {
event.stopPropagation();
const customStatusInputModalData = {
modalId: ModalIdentifiers.CUSTOM_STATUS,
dialogType: CustomStatusModal,
};
props.openModal(customStatusInputModalData);
}
return (
<div
id='lhsHeader'
aria-label={ariaLabel}
tabIndex={-1}
role='application'
className='SidebarHeader team__header theme a11y__region'
data-a11y-sort-order='5'
>
<div
className='d-flex'
>
<Contents/>
<div className='mobileSidebarHeader'>
<h1>{props.teamDisplayName}</h1>
<div className='mobileSidebarHeader__username'>
<span>{'@' + props.username}</span>
<CustomStatusEmoji
showTooltip={false}
emojiStyle={{
verticalAlign: 'top',
}}
onClick={handleCustomStatusEmojiClick}
/>
</div>
</div>
);

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

@@ -1,15 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {DeepPartial} from '@mattermost/types/utilities';
import {Preferences} from 'mattermost-redux/constants';
import {Permissions, Preferences} from 'mattermost-redux/constants';
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import {fireEvent, renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import Constants, {ModalIdentifiers} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
@@ -18,14 +17,61 @@ import type {GlobalState} from 'types/store';
import Sidebar from './sidebar';
describe('components/sidebar', () => {
const currentTeamId = 'fake_team_id';
const currentTeam = TestHelper.getTeamMock({
id: 'current_team_id',
display_name: 'Current Test Team',
});
const initialState: DeepPartial<GlobalState> = {
entities: {
teams: {
currentTeamId: currentTeam.id,
teams: {
[currentTeam.id]: currentTeam,
},
myMembers: {
[currentTeam.id]: {
roles: 'team_user',
},
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
id: 'current_user_id',
roles: 'system_user system_admin',
},
},
},
roles: {
roles: {
system_admin: {
permissions: [Permissions.MANAGE_TEAM],
},
system_user: {
permissions: [],
},
team_user: {
permissions: [],
},
},
},
preferences: {
myPreferences: {},
},
general: {
config: {},
},
},
};
const baseProps = {
canCreatePublicChannel: true,
canCreatePrivateChannel: true,
canJoinPublicChannel: true,
isOpen: false,
teamId: currentTeamId,
teamId: currentTeam.id,
hasSeenModal: true,
isCloud: false,
unreadFilterEnabled: false,
@@ -43,94 +89,36 @@ describe('components/sidebar', () => {
},
};
test('should match snapshot', () => {
const wrapper = shallow(
test('should render the sidebar components correctly', () => {
renderWithContext(
<Sidebar {...baseProps}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
// Check for SidebarContainer that is the parent of the sidebar
expect(document.getElementById('SidebarContainer')).toBeInTheDocument();
expect(screen.getByRole('application', {name: /channel sidebar region/i})).toBeInTheDocument();
});
test('should match snapshot when direct channels modal is open', () => {
const wrapper = shallow(
<Sidebar {...baseProps}/>,
);
wrapper.instance().setState({showDirectChannelsModal: true});
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot when more channels modal is open', () => {
const wrapper = shallow(
<Sidebar {...baseProps}/>,
);
wrapper.instance().setState({showMoreChannelsModal: true});
expect(wrapper).toMatchSnapshot();
});
test('Should call Shortcut modal on FORWARD_SLASH+ctrl/meta', () => {
const wrapper = shallow<Sidebar>(
<Sidebar {...baseProps}/>,
);
const instance = wrapper.instance();
let key = Constants.KeyCodes.BACK_SLASH[0] as string;
let keyCode = Constants.KeyCodes.BACK_SLASH[1] as number;
instance.handleKeyDownEvent({ctrlKey: true, preventDefault: jest.fn(), key, keyCode} as any);
expect(wrapper.instance().props.actions.openModal).not.toHaveBeenCalled();
key = 'ù';
keyCode = Constants.KeyCodes.FORWARD_SLASH[1] as number;
instance.handleKeyDownEvent({ctrlKey: true, preventDefault: jest.fn(), key, keyCode} as any);
expect(wrapper.instance().props.actions.openModal).toHaveBeenCalledWith(expect.objectContaining({modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL}));
key = '/';
keyCode = Constants.KeyCodes.SEVEN[1] as number;
instance.handleKeyDownEvent({ctrlKey: true, preventDefault: jest.fn(), key, keyCode} as any);
expect(wrapper.instance().props.actions.openModal).toHaveBeenCalledWith(expect.objectContaining({modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL}));
key = Constants.KeyCodes.FORWARD_SLASH[0] as string;
keyCode = Constants.KeyCodes.FORWARD_SLASH[1] as number;
instance.handleKeyDownEvent({ctrlKey: true, preventDefault: jest.fn(), key, keyCode} as any);
expect(wrapper.instance().props.actions.openModal).toHaveBeenCalledWith(expect.objectContaining({modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL}));
});
test('should toggle direct messages modal correctly', () => {
const wrapper = shallow<Sidebar>(
<Sidebar {...baseProps}/>,
);
const instance = wrapper.instance();
const mockEvent: Partial<Event> = {preventDefault: jest.fn()};
instance.hideMoreDirectChannelsModal = jest.fn();
instance.showMoreDirectChannelsModal = jest.fn();
instance.handleOpenMoreDirectChannelsModal(mockEvent as any);
expect(instance.showMoreDirectChannelsModal).toHaveBeenCalled();
instance.setState({showDirectChannelsModal: true});
instance.handleOpenMoreDirectChannelsModal(mockEvent as any);
expect(instance.hideMoreDirectChannelsModal).toHaveBeenCalled();
});
test('should match empty div snapshot when teamId is missing', () => {
test('should not rendering anything when teamId is missing', () => {
const props = {
...baseProps,
teamId: '',
};
const wrapper = shallow(
renderWithContext(
<Sidebar {...props}/>,
initialState,
);
expect(wrapper).toMatchSnapshot();
expect(screen.queryByRole('application', {name: /channel sidebar region/i})).toBeNull();
});
describe('unreads category', () => {
const currentUserId = 'current_user_id';
const channel1 = TestHelper.getChannelMock({id: 'channel1', team_id: currentTeamId});
const channel2 = TestHelper.getChannelMock({id: 'channel2', team_id: currentTeamId});
const channel1 = TestHelper.getChannelMock({id: 'channel1', team_id: currentTeam.id});
const channel2 = TestHelper.getChannelMock({id: 'channel2', team_id: currentTeam.id});
const baseState: DeepPartial<GlobalState> = {
entities: {
@@ -141,7 +129,7 @@ describe('components/sidebar', () => {
channel2,
},
channelsInTeam: {
[currentTeamId]: new Set([channel1.id, channel2.id]),
[currentTeam.id]: new Set([channel1.id, channel2.id]),
},
messageCounts: {
channel1: {total: 10},
@@ -153,13 +141,40 @@ describe('components/sidebar', () => {
},
},
teams: {
currentTeamId,
currentTeamId: currentTeam.id,
teams: {
[currentTeamId]: TestHelper.getTeamMock({id: currentTeamId}),
[currentTeam.id]: currentTeam,
},
myMembers: {
[currentTeam.id]: {
roles: 'team_user',
},
},
},
users: {
currentUserId,
profiles: {
[currentUserId]: {
id: currentUserId,
roles: 'system_user system_admin',
},
},
},
roles: {
roles: {
system_admin: {
permissions: [Permissions.MANAGE_TEAM],
},
system_user: {
permissions: [],
},
team_user: {
permissions: [],
},
},
},
general: {
config: {},
},
},
};
@@ -263,4 +278,111 @@ describe('components/sidebar', () => {
});
});
});
describe('modals', () => {
test('should call Shortcut modal on FORWARD_SLASH+ctrl/meta', () => {
const openModalSpy = jest.fn();
const closeModalSpy = jest.fn();
const props = {
...baseProps,
isKeyBoardShortcutModalOpen: false,
actions: {
...baseProps.actions,
openModal: openModalSpy,
closeModal: closeModalSpy,
},
};
renderWithContext(
<Sidebar {...props}/>,
initialState,
);
expect(document.getElementById('SidebarContainer')).toBeInTheDocument();
// Test with backslash key (should not trigger the modal)
fireEvent.keyDown(document, {
key: '\\',
code: 'Backslash',
keyCode: Constants.KeyCodes.BACK_SLASH[1],
ctrlKey: true,
});
expect(openModalSpy).not.toHaveBeenCalled();
// Test with 'ù' key but with forward slash keyCode (should trigger the modal)
fireEvent.keyDown(document, {
key: 'ù',
code: 'Slash',
keyCode: Constants.KeyCodes.FORWARD_SLASH[1],
ctrlKey: true,
});
expect(openModalSpy).toHaveBeenCalledWith(expect.objectContaining({
modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL,
}));
// Reset the spy
openModalSpy.mockClear();
// Test with '/' key but with seven keyCode (should trigger the modal)
fireEvent.keyDown(document, {
key: '/',
code: 'Digit7',
keyCode: Constants.KeyCodes.SEVEN[1],
ctrlKey: true,
});
expect(openModalSpy).toHaveBeenCalledWith(expect.objectContaining({
modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL,
}));
// Reset the spy
openModalSpy.mockClear();
// Test with forward slash key (should trigger the modal)
fireEvent.keyDown(document, {
key: '/',
code: 'Slash',
keyCode: Constants.KeyCodes.FORWARD_SLASH[1],
ctrlKey: true,
});
expect(openModalSpy).toHaveBeenCalledWith(expect.objectContaining({
modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL,
}));
});
test('should close Shortcut modal on FORWARD_SLASH+ctrl/meta when already open', () => {
const openModalSpy = jest.fn();
const closeModalSpy = jest.fn();
const props = {
...baseProps,
isKeyBoardShortcutModalOpen: true, // Modal is already open
actions: {
...baseProps.actions,
openModal: openModalSpy,
closeModal: closeModalSpy,
},
};
renderWithContext(
<Sidebar {...props}/>,
initialState,
);
expect(document.getElementById('SidebarContainer')).toBeInTheDocument();
// Test with forward slash key (should close the modal since it's already open)
fireEvent.keyDown(document, {
key: '/',
code: 'Slash',
keyCode: Constants.KeyCodes.FORWARD_SLASH[1],
ctrlKey: true,
});
// Should call closeModal with the keyboard shortcuts modal ID
expect(closeModalSpy).toHaveBeenCalledWith(ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL);
// Should not call openModal
expect(openModalSpy).not.toHaveBeenCalled();
});
});
});

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

@@ -17,7 +17,8 @@ import * as Menu from 'components/menu';
import {OnboardingTourSteps} from 'components/tours';
import {useShowOnboardingTutorialStep, CreateAndJoinChannelsTour, InvitePeopleTour} from 'components/tours/onboarding_tour';
export const ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU = 'browseOrAddChannelMenuButton';
export const ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU = 'browserOrAddChannelMenu';
export const ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU_BUTTON = 'browseOrAddChannelMenuButton';
type Props = {
canCreateChannel: boolean;
@@ -52,6 +53,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
/>
)}
trailingElements={showCreateAndJoinChannelsTutorialTip && <CreateAndJoinChannelsTour/>}
aria-haspopup='true'
/>
);
}
@@ -69,6 +71,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
defaultMessage='Browse channels'
/>
)}
aria-haspopup='true'
/>
);
}
@@ -84,6 +87,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
defaultMessage='Open a direct message'
/>
)}
aria-haspopup='true'
/>
);
@@ -100,6 +104,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
defaultMessage='Create new user group'
/>
)}
aria-haspopup='true'
/>
);
}
@@ -117,6 +122,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
defaultMessage='Create new category'
/>
)}
aria-haspopup='true'
/>
);
}
@@ -139,13 +145,14 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
</>
)}
trailingElements={showInvitePeopleTutorialTip && <InvitePeopleTour/>}
aria-haspopup='true'
/>
);
return (
<Menu.Container
menuButton={{
id: ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU,
id: ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU_BUTTON,
'aria-label': formatMessage({
id: 'sidebarLeft.browserOrCreateChannelMenuButton.label',
defaultMessage: 'Browse or create channels',
@@ -158,7 +165,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
}}
menu={{
id: 'browserOrAddChannelMenu',
'aria-labelledby': ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU,
'aria-labelledby': ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU_BUTTON,
}}
>
{createNewChannelMenuItem}

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

@@ -6,31 +6,31 @@
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.sidebarHeader {
display: flex;
margin: 0;
color: var(--sidebar-text);
cursor: pointer;
font-family: Metropolis, sans-serif;
font-size: 16px;
font-weight: 600;
line-height: 24px;
.title {
display: inline-block;
button#sidebarTeamMenuButton {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon-chevron-down {
margin-right: -1px;
margin-left: -3px;
}
#SidebarContainer & {
padding: 2px 6px;
font-family: Metropolis, sans-serif;
font-size: 16px;
gap: 1px;
& > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
& > .icon {
display: flex;
justify-content: center;
}
}
}
#sidebarTeamMenu {
.learnAboutTeamsMenuItem {
.leading-element,.label-elements>span {
color: var(--link-color);
}
}
}

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

@@ -1,141 +1,156 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import IconButton from '@mattermost/compass-components/components/icon-button'; // eslint-disable-line no-restricted-imports
import {Permissions} from 'mattermost-redux/constants';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {CloudProducts} from 'utils/constants';
import {FileSizes} from 'utils/file_utils';
import {TestHelper} from 'utils/test_helper';
import BrowserOrAddChannelMenu from './sidebar_browse_or_add_channel_menu';
import SidebarHeader from './sidebar_header';
import type {Props} from './sidebar_header';
let props: Props;
describe('SidebarHeader', () => {
const defaultProps: Props = {
showNewChannelModal: jest.fn(),
showMoreChannelsModal: jest.fn(),
invitePeopleModal: jest.fn(),
showCreateCategoryModal: jest.fn(),
canCreateChannel: true,
canJoinPublicChannel: true,
handleOpenDirectMessagesModal: jest.fn(),
unreadFilterEnabled: true,
showCreateUserGroupModal: jest.fn(),
canCreateCustomGroups: true,
};
const mockDispatch = jest.fn();
let mockState: any;
const team = TestHelper.getTeamMock({
display_name: 'Steadfast',
});
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
useDispatch: () => mockDispatch,
}));
describe('Components/SidebarHeader', () => {
beforeEach(() => {
props = {
showNewChannelModal: jest.fn(),
showMoreChannelsModal: jest.fn(),
invitePeopleModal: jest.fn(),
showCreateCategoryModal: jest.fn(),
canCreateChannel: true,
canJoinPublicChannel: true,
handleOpenDirectMessagesModal: jest.fn(),
unreadFilterEnabled: true,
showCreateUserGroupModal: jest.fn(),
canCreateCustomGroups: true,
};
mockState = {
entities: {
general: {
config: {},
const initialState = {
entities: {
general: {
config: {},
},
preferences: {
myPreferences: {},
},
teams: {
currentTeamId: team.id,
teams: {
[team.id]: team,
},
preferences: {
myPreferences: {},
myMembers: {
[team.id]: {
roles: 'team_user',
},
},
},
users: {
profiles: {
uid: {
id: 'uid',
roles: 'system_user system_admin',
},
},
currentUserId: 'uid',
},
roles: {
roles: {
system_admin: {
permissions: [Permissions.MANAGE_TEAM],
},
system_user: {
permissions: [],
},
team_user: {
permissions: [],
},
},
},
usage: {
integrations: {
enabled: 11,
enabledLoaded: true,
},
messages: {
history: 10000,
historyLoaded: true,
},
files: {
totalStorage: FileSizes.Gigabyte,
totalStorageLoaded: true,
},
teams: {
currentTeamId: 'currentteam',
teams: {
currentteam: {
id: 'currentteam',
description: 'et iste illum reprehenderit aliquid in rem itaque in maxime eius.',
},
active: 1,
teamsLoaded: true,
},
boards: {
cards: 500,
cardsLoaded: true,
},
},
cloud: {
subscription: {
product_id: 'test_prod_1',
trial_end_at: 1652807380,
is_free_trial: 'false',
},
products: {
test_prod_1: {
id: 'test_prod_1',
sku: CloudProducts.STARTER,
price_per_seat: 0,
},
},
users: {
profiles: {
uid: {},
},
currentUserId: 'uid',
},
usage: {
integrations: {
enabled: 11,
enabledLoaded: true,
},
messages: {
history: 10000,
historyLoaded: true,
},
files: {
totalStorage: FileSizes.Gigabyte,
totalStorageLoaded: true,
},
teams: {
active: 1,
teamsLoaded: true,
},
boards: {
cards: 500,
cardsLoaded: true,
},
},
cloud: {
subscription: {
product_id: 'test_prod_1',
trial_end_at: 1652807380,
is_free_trial: 'false',
},
products: {
test_prod_1: {
id: 'test_prod_1',
sku: CloudProducts.STARTER,
price_per_seat: 0,
},
},
limits: {
limitsLoaded: true,
limits: {
limitsLoaded: true,
limits: {
integrations: {
enabled: 10,
},
messages: {
history: 10000,
},
files: {
total_storage: FileSizes.Gigabyte,
},
teams: {
active: 1,
},
boards: {
cards: 500,
views: 5,
},
integrations: {
enabled: 10,
},
messages: {
history: 10000,
},
files: {
total_storage: FileSizes.Gigabyte,
},
teams: {
active: 1,
},
boards: {
cards: 500,
views: 5,
},
},
},
},
views: {
addChannelDropdown: {
isOpen: false,
},
},
};
},
};
test('should render the team menu button', () => {
renderWithContext(<SidebarHeader {...defaultProps}/>, initialState);
expect(screen.getByText('Steadfast')).toBeInTheDocument();
expect(screen.getByRole('button', {name: team.display_name})).toBeInTheDocument();
});
it('should show BrowserOrAddChannelMenu', () => {
const wrapper = shallow(<SidebarHeader {...props}/>);
expect(wrapper.find(BrowserOrAddChannelMenu).length).toBe(1);
test('should render the \'Browse or create channels\' menu button', () => {
renderWithContext(<SidebarHeader {...defaultProps}/>, initialState);
expect(screen.getByRole('button', {name: /Browse or create channels/i})).toBeInTheDocument();
});
it('should embed teams menu dropdown into heading', () => {
const wrapper = shallow(<SidebarHeader {...props}/>);
expect(wrapper.find(IconButton).length).toBe(0);
expect(wrapper.find('i').prop('className')).toBe('icon icon-chevron-down');
test('should not render anything when team is empty', () => {
const state = {...initialState};
state.entities.teams.currentTeamId = '';
renderWithContext(<SidebarHeader {...defaultProps}/>, state);
expect(screen.queryByRole('button', {name: team.display_name})).toBeNull();
expect(screen.queryByRole('button', {name: /Add Channel Dropdown/i})).toBeNull();
});
});

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше