MM-59065 - New channel menu using new menu system (#30093)

* New channel menu using new menu system

* fix e2e-tests

* remove extraneous separator

* lint fix

* fix test after merge

* update to pass properties to first menu item

* fix e2etest

* refactor: Update channel header menu items to use const event handlers

* refactor: Extract plugin item click handler in channel header menu

* refactor: Improve error handling and button click handlers in mobile channel header plugins

* lint fixes

* updates for code reveiw

* run i18n-extract

* fix unit test

* fix: Close channel dropdown menu by clicking channel header title

* fix: Use keyboard escape to close channel dropdown menu in e2e tests

* fix cypress test

* fix: Resolve MUI Menu component fragment rendering issue

* cleanup

* remove unneccessary css

* fixing testing issues

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Scott Bishel
2025-03-19 07:06:04 -05:00
коммит произвёл GitHub
родитель 8eadf849bb
Коммит fd717cfa64
133 изменённых файлов: 4279 добавлений и 6211 удалений

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

@@ -100,7 +100,8 @@ describe('Settings > Sidebar > General', () => {
cy.uiClose();
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
cy.uiOpenChannelMenu('Members');
cy.uiGetButton('Add').click();
// * Verify that the modal is open
cy.get('#addUsersToChannelModal').should('be.visible').findByText(`Add people to ${channel.display_name}`);

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

@@ -48,55 +48,41 @@ describe('Archive channel header spec', () => {
// * The dropdown menu of the channel header should be visible;
cy.get('#channelLeaveChannel').should('be.visible');
// * Rename menu option should be visible;
cy.get('#channelRename').should('be.visible');
// * Edit header menu option should be visible;
cy.get('#channelEditHeader').should('be.visible');
// * Edit purpose menu option should be visible;
cy.get('#channelEditPurpose').should('be.visible');
// * Convert channel to private menu option should be visible;
cy.get('#channelConvertToPrivate').should('be.visible');
// * Channel Settings submenu should be visible;
cy.get('#channelSettings').should('be.visible');
// * Archive channel menu option should be visible;
cy.get('#channelArchiveChannel').should('be.visible');
// * Add members menu option should be visible;
cy.get('#channelInviteMembers').should('be.visible');
// * Members menu option should be visible;
cy.get('#channelMembers').should('be.visible');
// * Notification preferences option should be visible;
cy.get('#channelNotificationPreferences').should('be.visible');
// # Close the channel dropdown menu
cy.get('#channelHeaderTitle').click();
// # Close the channel dropdown menu using keyboard escape
cy.get('body').type('{esc}{esc}');
// # Archive the channel
cy.uiArchiveChannel();
// * Rename menu option should not be visible;
cy.get('#channelRename').should('not.exist');
// * Edit header menu option should not be visible;
cy.get('#channelEditHeader').should('not.exist');
// * Edit purpose menu option should not be visible;
cy.get('#channelEditPurpose').should('not.exist');
// * Convert channel to private menu option should not be visible;
cy.get('#channelConvertToPrivate').should('not.exist');
// # click on channel drop-down menu
cy.get('#channelHeaderTitle').click();
// * Archive channel menu option should not be visible;
cy.get('#channelArchiveChannel').should('not.exist');
// * Add members menu option should not be visible;
cy.get('#channelInviteMembers').should('not.exist');
// * Channel Settings submenu should be visible;
cy.get('#channelSettings').should('not.exist');
// * Members menu option should be visible;
// as it now displays in RHS
cy.get('#channelMembers').should('be.visible');
// * Notification preferences option should not be visible;
cy.get('#channelNotificationPreferences').should('not.exist');
// # Close the channel dropdown menu
cy.get('#channelHeaderTitle').click();
// # Close the channel dropdown menu using keyboard escape
cy.get('body').type('{esc}{esc}');
});
});

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

@@ -28,14 +28,11 @@ describe('Archive channel members spec', () => {
// # click on channel drop-down menu
cy.get('#channelHeaderTitle').click();
// * View members menu option should not be visible;
cy.get('#channelViewMembers').should('not.exist');
// * Manage members menu option should be visible;
cy.get('#channelManageMembers').should('be.visible');
// * Members menu option should be visible;
cy.get('#channelMembers').should('be.visible');
// # Close the channel dropdown menu
cy.get('#channelHeaderTitle').click();
cy.get('body').type('{esc}{esc}');
// # Archive the channel
cy.uiArchiveChannel();
@@ -43,11 +40,8 @@ describe('Archive channel members spec', () => {
// # click on channel drop-down menu
cy.get('#channelHeaderTitle').click();
// * Manage members menu option should not be visible;
cy.get('#channelManageMembers').should('not.exist');
// # click on view members menu option;
cy.get('#channelViewMembers button').should('be.visible').click();
cy.get('#channelMembers').should('be.visible').click();
// * Ensure there are no options to change channel roles or membership
cy.uiGetRHS().findByText('Manage').should('not.exist');

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

@@ -101,7 +101,7 @@ describe('Channels', () => {
cy.get('#channelHeaderTitle').should('contain', testChannel.display_name).click();
// * Verify that the menu is opened
cy.get('.Menu__content').should('be.visible').within(() => {
cy.get('#channelHeaderDropdownMenu').should('be.visible').within(() => {
// # Archive the channel
cy.findByText('Archive Channel').should('be.visible').click();
});

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

@@ -30,9 +30,10 @@ describe('Channel Settings - Channel Header', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Click on the channel name in the channel header to open the channel menu options
cy.get(`[aria-label="${channel.name.split('-').join(' ')} channel menu"]`).click();
cy.get(`[aria-label="${channel.display_name.split('-').join(' ').toLowerCase()} channel menu"]`).click();
// # Select the "Edit Channel Header" option from the dropdown
cy.findByText('Channel Settings').should('be.visible').trigger('mouseover');
cy.findByText('Edit Channel Header').click();
// # Type something in the header edit box

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

@@ -205,15 +205,12 @@ describe('Channel members RHS', () => {
// # Go to test channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
cy.uiOpenChannelMenu('Manage Members');
cy.uiOpenChannelMenu('Members');
// * RHS Container should be open in edit mode
cy.get('#rhsContainer').then((rhsContainer) => {
cy.wrap(rhsContainer).findByText('Members').should('be.visible');
cy.wrap(rhsContainer).findByText(testChannel.display_name).should('be.visible');
// Done button should be visible
cy.wrap(rhsContainer).findByText('Done').should('be.visible');
});
});
@@ -267,7 +264,7 @@ describe('Channel members RHS', () => {
// # Go to test channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
cy.uiOpenChannelMenu('View Members');
cy.uiOpenChannelMenu('Members');
// * RHS Container should be open in edit mode
ensureChannelMembersRHSExists(testChannel);

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

@@ -38,7 +38,8 @@ describe('Channel Settings', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Go to channel dropdown > Rename channel
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
cy.findByText('Channel Settings').should('be.visible').trigger('mouseover');
cy.findByText('Rename Channel').click();
// # Try to enter existing URL and save
@@ -63,7 +64,7 @@ describe('Channel Settings', () => {
cy.visit(`/${testTeam.name}/channels/off-topic`);
// # Go to channel dropdown > Mute channel
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
cy.get('#channelHeaderDropdownMenu').should('exist').
findByText('Mute Channel').should('be.visible').click();
@@ -79,7 +80,7 @@ describe('Channel Settings', () => {
get('a').should('have.class', 'muted');
// # Click Unmute channel while menu is open
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
cy.get('#channelHeaderDropdownMenu').should('exist').
findByText('Unmute Channel').should('be.visible').click();

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

@@ -52,7 +52,7 @@ describe('Close direct messages', () => {
it('Through channel header dropdown menu', () => {
createAndVisitDMChannel([testUser.id, otherUser.id]).then((channel) => {
// # Open channel header dropdown menu and click on Close Direct Message
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
cy.findByText('Close Direct Message').click();
verifyChannelWasProperlyClosed(channel.name);
@@ -104,7 +104,7 @@ describe('Close group messages', () => {
it('Through channel header dropdown menu', () => {
createAndVisitGMChannel([otherUser1, otherUser2]).then((channel) => {
// # Open channel header dropdown menu and click on Close Direct Message
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
cy.findByText('Close Group Message').click();
verifyChannelWasProperlyClosed(channel.name);

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

@@ -41,14 +41,20 @@ describe('Channels', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Click the channel header dropdown
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// open ChannelSettings submenu
cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover');
// * Channel convert to private should be visible and confirm
cy.get('#channelConvertToPrivate').should('be.visible').click();
cy.findByTestId('convertChannelConfirm').should('be.visible').click();
// # Click the channel header dropdown
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// open ChannelSettings submenu
cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover');
// * Channel convert to private should no longer be visible
cy.get('#channelConvertToPrivate').should('not.exist');
@@ -66,7 +72,10 @@ describe('Channels', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Click the channel header dropdown
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// open ChannelSettings submenu
cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover');
// * Channel convert to private should no longer be visible
cy.get('#channelConvertToPrivate').should('not.exist');
@@ -91,7 +100,10 @@ describe('Channels', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Click the channel header dropdown
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// open ChannelSettings submenu
cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover');
// * Channel convert to private should not be visible
cy.get('#channelConvertToPrivate').should('not.exist');
@@ -109,14 +121,20 @@ describe('Channels', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Click the channel header dropdown
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// open ChannelSettings submenu
cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover');
// * Channel convert to private should be visible and confirm
cy.get('#channelConvertToPrivate').should('be.visible').click();
cy.findByTestId('convertChannelConfirm').should('be.visible').click();
// # Click the channel header dropdown
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// open ChannelSettings submenu
cy.findByRole('menuitem', {name: 'Channel Settings'}).trigger('mouseover');
// * Channel convert to private should no longer be visible
cy.get('#channelConvertToPrivate').should('not.exist');

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

@@ -60,6 +60,7 @@ describe('Group Message Conversion To Private Channel', () => {
// Open the GM conversion dialog
cy.get('#channelHeaderDropdownButton').click();
cy.findByRole('menuitem', {name: 'Settings'}).trigger('mouseover');
cy.findByText('Convert to Private Channel').click();
// the dialog has a animation, so we're waiting for it to finish
@@ -102,6 +103,7 @@ describe('Group Message Conversion To Private Channel', () => {
// Open the GM
cy.visit(`/${testTeam1.name}/messages/${gm2.name}`);
cy.get('#channelHeaderDropdownButton').click();
cy.findByRole('menuitem', {name: 'Settings'}).trigger('mouseover');
cy.findByText('Convert to Private Channel').click();
cy.get('.GenericModal__button.delete.disabled').wait(2000);
@@ -141,6 +143,7 @@ describe('Group Message Conversion To Private Channel', () => {
// Open the GM
cy.visit(`/${testTeam1.name}/messages/${gm3.name}`);
cy.get('#channelHeaderDropdownButton').click();
cy.findByRole('menuitem', {name: 'Settings'}).trigger('mouseover');
cy.findByText('Convert to Private Channel').click();
cy.findByText('Unable to convert to a channel because group members are part of different teams').wait(2000);
});

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

@@ -37,7 +37,7 @@ describe('Leave and Archive channel actions display as destructive', () => {
cy.findByText('View Info').should('be.visible');
// * Move to... menu option should be visible
cy.findByText('Move to...').should('be.visible').children().trigger('mouseover');
cy.findByText('Move to...').should('be.visible').trigger('mouseover');
// * Favorites Sub-menu option should be visible
cy.findByText('Favorites').should('be.visible');
@@ -45,17 +45,20 @@ describe('Leave and Archive channel actions display as destructive', () => {
// * New Category Sub-menu option should be visible
cy.findByText('New Category').should('be.visible');
// * Move to... close menu option
cy.findByText('Move to...').should('be.visible').trigger('mouseout');
// * Notification Preferences menu option should be visible
cy.get('#channelNotificationPreferences').should('be.visible');
// * Mute Channel menu option should be visible
cy.get('#channelToggleMuteChannel').should('be.visible');
// * Invite Members menu option should be visible
cy.get('#channelInviteMembers').should('be.visible');
// * Manage Members menu option should be visible
cy.get('#channelManageMembers').should('be.visible');
cy.get('#channelMembers').should('be.visible');
// * Channel Settings menu option should be visible
cy.findByText('Channel Settings').should('be.visible').trigger('mouseover');
// * Edit Channel Header menu option should be visible
cy.get('#channelEditHeader').should('be.visible');
@@ -66,11 +69,14 @@ describe('Leave and Archive channel actions display as destructive', () => {
// * Rename Channel menu option should be visible
cy.get('#channelRename').should('be.visible');
// * Archive Channel menu option should be visible and have a background-color (destructive)
cy.get('#channelArchiveChannel').should('be.visible').children().focus().should('have.css', 'background-color', 'rgb(210, 75, 78)');
// * Channel Settings close menu option
cy.findByText('Channel Settings').should('be.visible').trigger('mouseout');
// * Leave Channel menu option should be visible and hav a background-color (destructive)
cy.get('#channelLeaveChannel').should('be.visible').children().focus().should('have.css', 'background-color', 'rgb(210, 75, 78)');
// * Archive Channel menu option should be visible and have a background-color (destructive)
cy.get('#channelArchiveChannel').should('be.visible').focus().should('have.css', 'background-color', 'rgb(210, 75, 78)');
// // * Leave Channel menu option should be visible and hav a background-color (destructive)
cy.get('#channelLeaveChannel').should('be.visible').focus().should('have.css', 'background-color', 'rgb(210, 75, 78)');
});
it('MM-T4943_2 Leave channel actions display as destructive in the Edit Channel Menu ', () => {

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

@@ -60,49 +60,52 @@ describe('Channel Settings', () => {
});
});
it('MM-T859_1 Single User: Usernames are links, open profile popovers', () => {
// # Create and visit new channel
cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// it('MM-T859_1 Single User: Usernames are links, open profile popovers', () => {
// // # Create and visit new channel
// cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => {
// cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Add users to channel
addNumberOfUsersToChannel(1, false);
// // # Add users to channel
// addNumberOfUsersToChannel(1, false);
cy.getLastPostId().then((id) => {
// * The system message should contain 'added to the channel by you'
cy.get(`#postMessageText_${id}`).should('contain', 'added to the channel by you');
// cy.getLastPostId().then((id) => {
// // * The system message should contain 'added to the channel by you'
// cy.get(`#postMessageText_${id}`).should('contain', 'added to the channel by you');
// # Verify username link
verifyMentionedUserAndProfilePopover(id);
});
});
});
// // # Verify username link
// verifyMentionedUserAndProfilePopover(id);
// });
// });
// });
it('MM-T859_2 Combined Users: Usernames are links, open profile popovers', () => {
// # Create and visit new channel
cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// it('MM-T859_2 Combined Users: Usernames are links, open profile popovers', () => {
// // # Create and visit new channel
// cy.apiCreateChannel(testTeam.id, 'channel-test', 'Channel').then(({channel}) => {
// cy.visit(`/${testTeam.name}/channels/${channel.name}`);
addNumberOfUsersToChannel(3, false);
// addNumberOfUsersToChannel(3, false);
cy.getLastPostId().then((id) => {
cy.get(`#postMessageText_${id}`).should('contain', '2 others were added to the channel by you');
// cy.getLastPostId().then((id) => {
// cy.get(`#postMessageText_${id}`).should('contain', '2 others were added to the channel by you');
// # Click "2 others" to expand more users
cy.get(`#post_${id}`).find('.markdown__paragraph-inline').siblings('a').first().click().then(() => {
// # Verify each username link
verifyMentionedUserAndProfilePopover(id);
});
});
});
});
// // # Click "2 others" to expand more users
// cy.get(`#post_${id}`).find('.markdown__paragraph-inline').siblings('a').first().click().then(() => {
// // # Verify each username link
// verifyMentionedUserAndProfilePopover(id);
// });
// });
// });
// });
it('MM-T856_1 Add existing users to public channel from drop-down > Add Members', () => {
// # Visit the add users channel
cy.visit(`/${testTeam.name}/channels/${addedUsersChannel.name}`);
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
cy.uiOpenChannelMenu('Members');
cy.uiGetButton('Add').click();
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');
// # Type into the input box to search for a user
@@ -160,7 +163,10 @@ describe('Channel Settings', () => {
cy.visit(`/${testTeam.name}/channels/off-topic`);
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
cy.uiOpenChannelMenu('Members');
cy.uiGetButton('Add').click();
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');
// # Type into the input box to search for already added user
@@ -182,7 +188,8 @@ describe('Channel Settings', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
cy.uiOpenChannelMenu('Members');
cy.uiGetButton('Add').click();
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');
@@ -230,7 +237,8 @@ describe('Channel Settings', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
cy.uiOpenChannelMenu('Members');
cy.uiGetButton('Add').click();
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');
@@ -275,7 +283,8 @@ describe('Channel Settings', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
cy.uiOpenChannelMenu('Members');
cy.uiGetButton('Add').click();
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');
@@ -324,7 +333,8 @@ describe('Channel Settings', () => {
cy.visit(`/${testTeam.name}/channels/${channel.name}`);
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
cy.uiOpenChannelMenu('Members');
cy.uiGetButton('Add').click();
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');
@@ -369,7 +379,8 @@ function verifyMentionedUserAndProfilePopover(postId: string) {
function addNumberOfUsersToChannel(num = 1, allowExisting = false) {
// # Open channel menu and click 'Add Members'
cy.uiOpenChannelMenu('Add Members');
cy.uiOpenChannelMenu('Members');
cy.uiGetButton('Add').click();
cy.get('#addUsersToChannelModal').should('be.visible');
// * Assert that modal appears

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

@@ -35,7 +35,10 @@ describe('Channel routing', () => {
cy.uiCreateChannel({name: 'Test__Channel'});
// # Click on channel menu and press rename channel
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// * Channel Settings menu option should be visible
cy.findByText('Channel Settings').should('be.visible').trigger('mouseover');
cy.findByText('Rename Channel').click();
// # Assert if the rename modal present
@@ -97,7 +100,10 @@ describe('Channel routing', () => {
// # In a test channel, click the "v" to the right of the channel name in the header
cy.findByText(`${testChannel.display_name}`).click();
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// * Channel Settings menu option should be visible
cy.findByText('Channel Settings').should('be.visible').trigger('mouseover');
// # Select "Rename Channel"
cy.findByText('Rename Channel').click();
@@ -113,7 +119,10 @@ describe('Channel routing', () => {
cy.get('#channelHeaderTitle').contains(`${firstWord} ${secondWord}`);
// # In a test channel, click the "v" to the right of the channel name in the header
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// * Channel Settings menu option should be visible
cy.findByText('Channel Settings').should('be.visible').trigger('mouseover');
// # Select "Rename Channel"
cy.findByText('Rename Channel').click();

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

@@ -71,7 +71,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
cy.visit(`/${testTeam.name}/channels/off-topic`);
// # Open Channel Members Dialog
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
cy.findByText('Manage Members').click().wait(TIMEOUTS.FIVE_SEC);
// * Verify the accessibility support in Manage Members Dialog
@@ -118,7 +118,7 @@ function verifyMainMenuModal(menuItem: string, modalName?: string) {
}
function verifyChannelMenuModal(menuItem: string, modalName?: string) {
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
verifyModal(menuItem, modalName);
}

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

@@ -149,8 +149,8 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => {
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
// # Open Add Members Dialog
cy.get('#channelHeaderDropdownIcon').click();
cy.findByText('Add Members').click();
cy.uiOpenChannelMenu('Members');
cy.uiGetButton('Add').click();
// * Verify the accessibility support in Add people Dialog
cy.findAllByRole('dialog').eq(0).within(() => {

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

@@ -59,8 +59,8 @@ describe('Verify Guest User Identification in different screens', () => {
it('MM-T1370 Verify Guest Badge in Channel Members dropdown and dialog', () => {
// # Open Channel Members RHS
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelManageMembers').click().wait(TIMEOUTS.HALF_SEC);
cy.get('#channelHeaderTitle').click();
cy.get('#channelMembers').click().wait(TIMEOUTS.HALF_SEC);
cy.uiGetRHS().findByTestId(`memberline-${guestUser.id}`).within(($el) => {
cy.wrap($el).get('.Tag').should('be.visible').should('have.text', 'GUEST');
});

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

@@ -118,16 +118,10 @@ describe('Team Permissions', () => {
cy.uiOpenChannelMenu().wait(TIMEOUTS.HALF_SEC);
// * Verify dropdown opens
cy.get('#channelHeaderDropdownMenu .Menu__content.dropdown-menu').should('be.visible');
// * Verify `Add Members` menu item is not present
cy.get('#channelAddMembers').should('not.exist');
// * Verify `Manage Members` menu item is not present
cy.get('#channelManageMembers').should('not.exist');
cy.get('#channelHeaderDropdownMenu').should('be.visible');
// * Verify `View Members` menu item is visible
cy.get('#channelViewMembers').should('be.visible');
cy.get('#channelMembers').should('be.visible');
// # Close channel header menu
cy.get('body').type('{esc}');
@@ -166,10 +160,11 @@ describe('Team Permissions', () => {
cy.uiOpenChannelMenu().wait(TIMEOUTS.HALF_SEC);
// * Verify dropdown opens
cy.get('#channelHeaderDropdownMenu .Menu__content.dropdown-menu').should('be.visible');
cy.get('#channelHeaderDropdownMenu').should('be.visible');
// # Click on `Invite Members`
cy.get('#channelInviteMembers').should('be.visible').click().wait(TIMEOUTS.HALF_SEC);
// # Click on `Members`
cy.get('#channelMembers').should('be.visible').click().wait(TIMEOUTS.HALF_SEC);
cy.uiGetButton('Add').click();
// # Search and select otherUser
cy.get('#selectItems input').typeWithForce(otherUser.username).wait(TIMEOUTS.HALF_SEC);
@@ -182,10 +177,11 @@ describe('Team Permissions', () => {
cy.uiOpenChannelMenu().wait(TIMEOUTS.HALF_SEC);
// * Verify dropdown opens
cy.get('#channelHeaderDropdownMenu .Menu__content.dropdown-menu').should('be.visible');
cy.get('#channelHeaderDropdownMenu').should('be.visible');
// # Click on `Manage Members`
cy.get('#channelManageMembers').should('be.visible').click().wait(TIMEOUTS.HALF_SEC);
// # Click on `Members`
cy.get('#channelMembers').should('be.visible').click().wait(TIMEOUTS.HALF_SEC);
cy.uiGetButton('Manage').click();
// # Click on `Member`
cy.uiGetRHS().findByTestId(`memberline-${otherUser.id}`).within(() => {
@@ -210,7 +206,8 @@ describe('Team Permissions', () => {
cy.uiOpenChannelMenu().wait(TIMEOUTS.HALF_SEC);
// * Verify dropdown opens
cy.get('#channelHeaderDropdownMenu .Menu__content.dropdown-menu').should('be.visible');
cy.get('#channelHeaderDropdownMenu').should('be.visible');
cy.findByText('Channel Settings').should('be.visible').trigger('mouseover');
// * Verify `Edit Channel Header` menu item is visible
cy.get('#channelEditHeader').should('be.visible');

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

@@ -125,7 +125,7 @@ function verifyRenamePrivateChannel(teamName, privateChannelName, testUsers) {
cy.visit(`/${teamName}/channels/${privateChannelName}`);
// * Click the dropdown menu and verify if the rename option is visible or not
cy.get('#channelHeaderDropdownIcon', {timeout: TIMEOUTS.TWO_MIN}).should('be.visible').click();
cy.get('#channelHeaderTitle', {timeout: TIMEOUTS.TWO_MIN}).should('be.visible').click();
cy.get('#channelRename').should(canRename ? 'be.visible' : 'not.exist');
}
}

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

@@ -115,8 +115,13 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag
verifyEmojiPickerNotOpen();
['Edit Channel Header', 'Rename Channel'].forEach((modal) => {
// # Open the modal and do keyboard shortcut
cy.uiOpenChannelMenu(modal);
// # Click on the channel name in the channel header to open the channel menu options
cy.get('#channelHeaderTitle').click();
// # Select the "Edit Channel Header" option from the dropdown
cy.findByText('Channel Settings').should('be.visible').trigger('mouseover');
cy.findByText(modal).click();
doReactToLastMessageShortcut();
// * Verify emoji picker is not open
@@ -135,6 +140,9 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag
// # Verify emoji picker is not open
cy.get('#emojiPicker').should('not.exist');
// close channel Menu
pressEscapeKey();
// * Open the main menu dropdown and do keyboard shortcut
cy.uiOpenTeamMenu();
doReactToLastMessageShortcut();

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

@@ -61,7 +61,7 @@ describe('Channels', () => {
cy.get('#channelHeaderTitle').should('contain', testChannel.display_name).click();
// * Verify that the menu is opened
cy.get('.Menu__content').should('be.visible').within(() => {
cy.get('#channelHeaderDropdownMenu').should('be.visible').within(() => {
// # Archive the channel
cy.findByText('Archive Channel').should('be.visible').click();
});

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

@@ -170,14 +170,14 @@ describe('Direct Message', () => {
// # Visit the DM channel
cy.visit(`/${testTeam.name}/messages/@${otherUser.username}`);
// # Open channel menu and click Mute Conversation
cy.uiOpenChannelMenu('Mute Conversation');
// # Open channel menu and click Mute
cy.uiOpenChannelMenu('Mute');
// * Assert that channel appears as muted on the LHS
cy.uiGetLhsSection('DIRECT MESSAGES').find('.muted').first().should('contain', otherUser.username);
// # Open channel menu and click Unmute Conversation
cy.uiOpenChannelMenu('Unmute Conversation');
// # Open channel menu and click Unmute
cy.uiOpenChannelMenu('Unmute');
// * Assert that channel does not appear as muted on the LHS
cy.uiGetLhsSection('DIRECT MESSAGES').find('.muted').should('not.exist');

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

@@ -142,7 +142,8 @@ function verifyFocusInAddChannelMemberModal() {
cy.get('#channelLeaveChannel').should('be.visible');
// # Click 'Add Members'
cy.get('#channelInviteMembers').click();
cy.get('#channelMembers').click();
cy.uiGetButton('Add').click();
// * Assert that modal appears
cy.get('#addUsersToChannelModal').should('be.visible');

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

@@ -30,16 +30,16 @@ describe('Header', () => {
it('MM-T88 An ellipsis indicates the channel header is too long - public or private channel Quote icon displays at beginning of channel header', () => {
// * Verify with short channel header
updateAndVerifyChannelHeader('>', 'newheader');
updateAndVerifyChannelHeader(false, '>', 'newheader');
// * Verify with long channel header
updateAndVerifyChannelHeader('>', 'newheader'.repeat(20));
updateAndVerifyChannelHeader(false, '>', 'newheader'.repeat(20));
});
it('MM-T881_1 - Header: Markdown quote', () => {
// # Update channel header text
const header = 'This is a quote in the header';
updateAndVerifyChannelHeader('>', header);
updateAndVerifyChannelHeader(false, '>', header);
});
it('MM-T89 An ellipsis indicates the channel header is too long - DM', () => {
@@ -56,7 +56,7 @@ describe('Header', () => {
// # Update DM channel header
const header = `quote ${'newheader'.repeat(15)}`;
updateAndVerifyChannelHeader('>', header);
updateAndVerifyChannelHeader(true, '>', header);
// # Hover on channel header
cy.get('#channelHeaderDescription .header-description__text').trigger('mouseenter');
@@ -70,9 +70,13 @@ describe('Header', () => {
});
});
function updateAndVerifyChannelHeader(prefix, header) {
function updateAndVerifyChannelHeader(isDM, prefix, header) {
// # Update channel header
cy.updateChannelHeader(prefix + header);
if (isDM) {
cy.updateDMGMChannelHeader(prefix + header);
} else {
cy.updateChannelHeader(prefix + header);
}
// * Should render blockquote if it starts with ">"
if (prefix === '>') {

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

@@ -44,7 +44,7 @@ describe('Multi-user group messages', () => {
cy.get('#saveItems').click();
// # Click channel dropdown icon to open menu
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// # Click 'close' item
cy.findByText('Close Group Message').should('be.visible').click();

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

@@ -108,7 +108,8 @@ describe('Multi-user group messages', () => {
cy.postMessage('messages');
// # Open channel menu and click Add Members
cy.uiOpenChannelMenu('Add Members');
cy.uiOpenChannelMenu('Members');
cy.uiGetButton('Add').click();
// * Verify message says: "This will start a new conversation. If you're adding a lot of people, consider creating a private channel instead."
cy.get('#moreDmModal').should('be.visible');
@@ -139,7 +140,6 @@ describe('Multi-user group messages', () => {
cy.contains('p.channel-intro__text', 'This is the start of your group message history with');
// * New user is added to the GM
cy.get('.member-rhs__trigger').click();
cy.uiGetRHS().contains(testUser.username).should('be.visible');
// * Other users are still there

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

@@ -136,8 +136,10 @@ describe('Multi-user group header', () => {
});
function editHeader(header) {
// # Click edit conversation header
cy.uiOpenChannelMenu('Edit Conversation Header');
// # Click edit header
cy.uiOpenChannelMenu('');
cy.findByRole('menuitem', {name: 'Settings'}).trigger('mouseover');
cy.findByText('Edit Header').click();
// * Verify the modal open to add header
cy.get('#editChannelHeaderModalLabel').should('be.visible').wait(TIMEOUTS.ONE_SEC);

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

@@ -62,10 +62,11 @@ describe('Multi-user group messages', () => {
and('have.css', 'text-overflow', 'ellipsis');
// # From the group message's window, click on the user list's dropdown
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
// # From the dropdown menu, click on "Add members"
cy.get('#channelAddMembers').click();
cy.get('#channelMembers').click();
cy.uiGetButton('Add').click();
// # Try to add one more user from the group message's list
addUsersToGMViaModal(1);

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

@@ -72,7 +72,7 @@ describe('System Console > User Management > Deactivation', () => {
cy.sendDirectMessageToUser(other, MESSAGES.SMALL);
// # Close DM
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
cy.findByText('Close Direct Message').click();
// # Deactivate the user

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

@@ -56,6 +56,9 @@ describe('channels > general actions', {testIsolation: true}, () => {
// # Open Channel Header and the Channel Actions modal
cy.get('#channelHeaderTitle').click();
// * More actions menu option should be visible
cy.findByText('More actions').should('be.visible').trigger('mouseover');
cy.findByText('Channel Actions').click();
// # Enable the categorization action and set the name
@@ -87,6 +90,9 @@ describe('channels > general actions', {testIsolation: true}, () => {
// # Open Channel Header and the Channel Actions modal
cy.get('#channelHeaderTitle').click();
// * More actions menu option should be visible
cy.findByText('More actions').should('be.visible').trigger('mouseover');
cy.findByText('Channel Actions').click();
// # Toggle on and set the welcome message
@@ -136,6 +142,9 @@ describe('channels > general actions', {testIsolation: true}, () => {
// # Open Channel Header and the Channel Actions modal
cy.get('#channelHeaderTitle').click();
// * More actions menu option should be visible
cy.findByText('More actions').should('be.visible').trigger('mouseover');
cy.findByText('Channel Actions').click();
// # Set a keyword, enable the playbook trigger,
@@ -196,6 +205,9 @@ describe('channels > general actions', {testIsolation: true}, () => {
// # Open Channel Header and the Channel Actions modal
cy.get('#channelHeaderTitle').click();
// * More actions menu option should be visible
cy.findByText('More actions').should('be.visible').trigger('mouseover');
cy.findByText('Channel Actions').click();
// # Set a keyword, enable the playbook trigger,
@@ -272,6 +284,9 @@ describe('channels > general actions', {testIsolation: true}, () => {
// # Open Channel Header and the Channel Actions modal
cy.get('#channelHeaderTitle').click();
// * More actions menu option should be visible
cy.findByText('More actions').should('be.visible').trigger('mouseover');
cy.findByText('Channel Actions').click();
// # Set a keyword, enable the playbook trigger,
@@ -311,6 +326,9 @@ describe('channels > general actions', {testIsolation: true}, () => {
// # Open Channel Header and the Channel Actions modal
cy.get('#channelHeaderTitle').click();
// * More actions menu option should be visible
cy.findByText('More actions').should('be.visible').trigger('mouseover');
cy.findByText('Channel Actions').click();
// * Verify the toggles are disabled
@@ -333,6 +351,9 @@ describe('channels > general actions', {testIsolation: true}, () => {
// # Open Channel Header and the Channel Actions modal
cy.get('#channelHeaderTitle').click();
// * More actions menu option should be visible
cy.findByText('More actions').should('be.visible').trigger('mouseover');
cy.findByText('Channel Actions').click();
// # Enable the categorization action and set the name
@@ -353,6 +374,9 @@ describe('channels > general actions', {testIsolation: true}, () => {
// # Open Channel Header and the Channel Actions modal
cy.get('#channelHeaderTitle').click();
// * More actions menu option should be visible
cy.findByText('More actions').should('be.visible').trigger('mouseover');
cy.findByText('Channel Actions').click();
// * Verify that the categorization action is disabled

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

@@ -104,6 +104,7 @@ describe('channels > run', {testIsolation: true}, () => {
// # Open the channel header
cy.get('#channelHeaderTitle').click();
cy.findByText('Channel Settings').should('be.visible').trigger('mouseover');
// * Verify the ability to edit the channel header exists
cy.get('#channelEditHeader').should('exist');

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

@@ -43,8 +43,9 @@ Cypress.Commands.add('uiCreateChannel', ({
Cypress.Commands.add('uiAddUsersToCurrentChannel', (usernameList) => {
if (usernameList.length) {
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelAddMembers').click();
cy.get('#channelHeaderTitle').click();
cy.get('#channelMembers').click();
cy.uiGetButton('Add').click();
cy.get('#addUsersToChannelModal').should('be.visible');
usernameList.forEach((username) => {
cy.get('#selectItems input').typeWithForce(`@${username}{enter}`);
@@ -56,8 +57,9 @@ Cypress.Commands.add('uiAddUsersToCurrentChannel', (usernameList) => {
Cypress.Commands.add('uiInviteUsersToCurrentChannel', (usernameList) => {
if (usernameList.length) {
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelInviteMembers').click();
cy.get('#channelHeaderTitle').click();
cy.get('#channelMembers').click();
cy.uiGetButton('Add').click();
cy.get('#addUsersToChannelModal').should('be.visible');
usernameList.forEach((username) => {
cy.get('#selectItems input').typeWithForce(`@${username}{enter}`);
@@ -68,19 +70,19 @@ Cypress.Commands.add('uiInviteUsersToCurrentChannel', (usernameList) => {
});
Cypress.Commands.add('uiArchiveChannel', () => {
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
cy.get('#channelArchiveChannel').click();
return cy.get('#deleteChannelModalDeleteButton').click();
});
Cypress.Commands.add('uiUnarchiveChannel', () => {
cy.get('#channelHeaderDropdownIcon').should('be.visible').click();
cy.get('#channelHeaderTitle').should('be.visible').click();
cy.get('#channelUnarchiveChannel').should('be.visible').click();
return cy.get('#unarchiveChannelModalDeleteButton').should('be.visible').click();
});
Cypress.Commands.add('uiLeaveChannel', (isPrivate = false) => {
cy.get('#channelHeaderDropdownIcon').click();
cy.get('#channelHeaderTitle').click();
if (isPrivate) {
cy.get('#channelLeaveChannel').click();

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

@@ -32,7 +32,6 @@ Cypress.Commands.add('uiGetChannelFileButton', () => {
Cypress.Commands.add('uiGetChannelMenu', (options = {exist: true}) => {
if (options.exist) {
return cy.get('#channelHeaderDropdownMenu').
find('.dropdown-menu').
should('be.visible');
}

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

@@ -462,13 +462,15 @@ function getCurrentChannelId(): ChainableT<string> {
Cypress.Commands.add('getCurrentChannelId', getCurrentChannelId);
function updateChannelHeader(text: string) {
cy.get('#channelHeaderDropdownIcon').
cy.get('#channelHeaderTitle').
should('be.visible').
click();
cy.get('.Menu__content').
should('be.visible').
find('#channelEditHeader').
click();
cy.get('#channelHeaderDropdownMenu').
should('be.visible');
// * Channel Settings menu option should be visible
cy.findByText('Channel Settings').should('be.visible').trigger('mouseover');
cy.findByText('Edit Channel Header').click();
cy.get('#edit_textbox').
clear().
type(text).
@@ -478,6 +480,24 @@ function updateChannelHeader(text: string) {
Cypress.Commands.add('updateChannelHeader', updateChannelHeader);
function updateDMGMChannelHeader(text: string) {
cy.get('#channelHeaderTitle').
should('be.visible').
click();
cy.get('#channelHeaderDropdownMenu').
should('be.visible');
// * Channel Settings menu option should be visible
cy.findByText('Edit Header').click();
cy.get('#edit_textbox').
clear().
type(text).
type('{enter}').
wait(TIMEOUTS.HALF_SEC);
}
Cypress.Commands.add('updateDMGMChannelHeader', updateDMGMChannelHeader);
function checkRunLDAPSync(): ChainableT<any> {
return cy.apiGetLDAPSync().then((response) => {
const jobs = response.body;
@@ -787,6 +807,12 @@ declare global {
*/
updateChannelHeader(text: string): ChainableT<void>;
/**
* Update DM or GM channel header
* @param {String} text - Text to set the header to
*/
updateDMGMChannelHeader(text: string): ChainableT<void>;
/**
* Navigate to system console-PluginManagement from profile settings
*/

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

@@ -1,75 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Purpose of this file to exists is only required until channel header dropdown is migrated to new menus
import type {ComponentProps} from 'react';
import React, {memo} from 'react';
import {useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import {
LinkVariantIcon,
PaperclipIcon,
} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import {getChannelBookmarks} from 'mattermost-redux/selectors/entities/channel_bookmarks';
import {useBookmarkAddActions} from 'components/channel_bookmarks/channel_bookmarks_menu';
import {MAX_BOOKMARKS_PER_CHANNEL, useCanUploadFiles, useChannelBookmarkPermission} from 'components/channel_bookmarks/utils';
import Menu from 'components/widgets/menu/menu';
import type {GlobalState} from 'types/store';
type Props = {
channel: Channel;
inHeaderDropdown?: boolean;
};
const ChannelBookmarksSubmenu = (props: Props) => {
const {formatMessage} = useIntl();
const {handleCreateLink, handleCreateFile} = useBookmarkAddActions(props.channel.id);
const canAdd = useChannelBookmarkPermission(props.channel.id, 'add');
const canUploadFiles = useCanUploadFiles();
const limitReached = useSelector((state: GlobalState) => {
const bookmarks = getChannelBookmarks(state, props.channel.id);
return bookmarks && Object.keys(bookmarks).length >= MAX_BOOKMARKS_PER_CHANNEL;
});
if (!canAdd || limitReached) {
return null;
}
const items: ComponentProps<typeof Menu.ItemSubMenu>['subMenu'] = [
{
id: 'channelBookmarksAddLink',
icon: <LinkVariantIcon size={16}/>,
direction: 'right',
text: formatMessage({id: 'channel_bookmarks.addLink', defaultMessage: 'Add a link'}),
action: handleCreateLink,
},
];
if (canUploadFiles) {
items.push({
id: 'channelBookmarksAttachFile',
icon: <PaperclipIcon size={16}/>,
direction: 'right',
text: formatMessage({id: 'channel_bookmarks.attachFile', defaultMessage: 'Attach a file'}),
action: handleCreateFile,
});
}
return (
<Menu.ItemSubMenu
id={`channel-menu-${props.channel.id}-bookmarks`}
subMenu={items}
text={formatMessage({id: 'sidebar_left.sidebar_channel_menu.bookmarks', defaultMessage: 'Bookmarks Bar'})}
direction={'right'}
styleSelectableItem={true}
/>
);
};
export default memo(ChannelBookmarksSubmenu);

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

@@ -1,27 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import type {ReactNode} from 'react';
import React, {memo, useState, useRef, useEffect} from 'react';
import {useIntl} from 'react-intl';
import React, {memo} from 'react';
import {useSelector} from 'react-redux';
import type {UserProfile} from '@mattermost/types/users';
import {Client4} from 'mattermost-redux/client';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {getIsRhsOpen} from 'selectors/rhs';
import {ChannelHeaderDropdown} from 'components/channel_header_dropdown';
import ProfilePicture from 'components/profile_picture';
import SharedChannelIndicator from 'components/shared_channel_indicator';
import ArchiveIcon from 'components/widgets/icons/archive_icon';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import BotTag from 'components/widgets/tag/bot_tag';
import WithTooltip from 'components/with_tooltip';
import {Constants} from 'utils/constants';
@@ -29,6 +21,8 @@ import ChannelHeaderTitleDirect from './channel_header_title_direct';
import ChannelHeaderTitleFavorite from './channel_header_title_favorite';
import ChannelHeaderTitleGroup from './channel_header_title_group';
import ChannelHeaderMenu from '../channel_header_menu/channel_header_menu';
type Props = {
dmUser?: UserProfile;
gmMembers?: UserProfile[];
@@ -38,45 +32,22 @@ const ChannelHeaderTitle = ({
dmUser,
gmMembers,
}: Props) => {
const [titleMenuOpen, setTitleMenuOpen] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const intl = useIntl();
const channel = useSelector(getCurrentChannel);
const currentUser = useSelector(getCurrentUser);
const headerItemRef = useRef<HTMLElement | null>(null);
const isRHSOpen = useSelector(getIsRhsOpen);
useEffect(() => {
enableToolTipIfNeeded();
// Re-check on window resize
const handleResize = () => enableToolTipIfNeeded();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [channel, gmMembers, dmUser, isRHSOpen]);
if (!channel) {
return null;
}
const enableToolTipIfNeeded = () => {
const element = headerItemRef.current;
const isTooltip = element && element.offsetWidth < element.scrollWidth;
setShowTooltip(isTooltip as boolean);
};
const isDirect = (channel.type === Constants.DM_CHANNEL);
const isGroup = (channel.type === Constants.GM_CHANNEL);
const channelIsArchived = channel.delete_at !== 0;
let archivedIcon: React.ReactNode = null;
let archivedIcon;
if (channelIsArchived) {
archivedIcon = <ArchiveIcon className='icon icon__archive icon channel-header-archived-icon svg-text-color'/>;
}
let sharedIcon = null;
let sharedIcon;
if (channel.shared) {
sharedIcon = (
<SharedChannelIndicator
@@ -118,20 +89,6 @@ const ChannelHeaderTitle = ({
);
}
const personalChannelHeaderAriaLabel = intl.formatMessage({
id: 'channel_header.directchannel',
defaultMessage: '{displayName} (you) Channel Menu',
}, {
displayName: channel.display_name,
});
const othersChannelHeaderAriaLabel = intl.formatMessage({
id: 'channel_header.otherchannel',
defaultMessage: '{displayName} Channel Menu',
}, {
displayName: channel.display_name,
});
return (
<div className='channel-header__top'>
<ChannelHeaderTitleFavorite/>
@@ -142,60 +99,12 @@ const ChannelHeaderTitle = ({
status={channel.status}
/>
)}
<MenuWrapper onToggle={setTitleMenuOpen}>
<div
id='channelHeaderDropdownButton'
>
<button
id='channelHeaderMenuButton'
className={classNames('channel-header__trigger style--none', {active: titleMenuOpen})}
aria-label={
(isDirect && currentUser.id === dmUser?.id) ? personalChannelHeaderAriaLabel.toLowerCase() : othersChannelHeaderAriaLabel.toLowerCase()
}
aria-expanded={titleMenuOpen}
aria-controls='channelHeaderDropdownMenu'
>
{showTooltip ? (
<WithTooltip
title={channelTitle as string}
>
<strong
id='channelHeaderTitle'
className='heading'
ref={headerItemRef}
>
<span>
{archivedIcon}
{channelTitle}
{sharedIcon}
</span>
</strong>
</WithTooltip>
) : (
<strong
id='channelHeaderTitle'
className='heading'
ref={headerItemRef}
>
<span>
{archivedIcon}
{channelTitle}
{sharedIcon}
</span>
</strong>
)}
<span
id='channelHeaderDropdownIcon'
className='icon icon-chevron-down header-dropdown-chevron-icon'
aria-hidden='true'
/>
</button>
</div>
<ChannelHeaderDropdown
ariaLabel={
(isDirect && currentUser.id === dmUser?.id) ? personalChannelHeaderAriaLabel.toLowerCase() : othersChannelHeaderAriaLabel.toLowerCase()}
/>
</MenuWrapper>
<ChannelHeaderMenu
dmUser={dmUser}
gmMembers={gmMembers}
sharedIcon={sharedIcon}
archivedIcon={archivedIcon}
/>
</div>
);
};

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

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

@@ -1,43 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {TestHelper} from 'utils/test_helper';
import ChannelHeaderDropdown from './channel_header_dropdown_items';
import type {Props} from './channel_header_dropdown_items';
describe('components/ChannelHeaderDropdown', () => {
const defaultProps = {
user: TestHelper.getUserMock({id: 'test-user-id'}),
channel: TestHelper.getChannelMock({id: 'test-channel-id'}),
isDefault: false,
isFavorite: false,
isReadonly: false,
isMuted: false,
isArchived: false,
isMobile: false,
penultimateViewedChannelName: 'test-channel',
pluginMenuItems: [],
isLicensedForLDAPGroups: false,
isChannelBookmarksEnabled: false,
};
test('should match snapshot with no plugin items', () => {
const wrapper = shallow(<ChannelHeaderDropdown {...defaultProps}/>);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot with plugins', () => {
const props: Props = {
...defaultProps,
pluginMenuItems: [
{id: 'plugin-1', pluginId: 'playbooks', action: jest.fn(), text: 'plugin-1-text', shouldRender: () => true},
{id: 'plugin-2', pluginId: 'playbooks', action: jest.fn(), text: 'plugin-2-text', shouldRender: () => true},
],
};
const wrapper = shallow(<ChannelHeaderDropdown {...props}/>);
expect(wrapper).toMatchSnapshot();
});
});

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

@@ -1,21 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {ChannelHeaderDropdownItems} from 'components/channel_header_dropdown';
import Menu from 'components/widgets/menu/menu';
const ChannelHeaderDropdown = ({ariaLabel}: {
ariaLabel: string;
}) =>
(
<Menu
id='channelHeaderDropdownMenu'
ariaLabel={ariaLabel}
>
<ChannelHeaderDropdownItems isMobile={false}/>
</Menu>
);
export default memo(ChannelHeaderDropdown);

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

@@ -1,355 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {Channel} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import {Permissions} from 'mattermost-redux/constants';
import {isGuest} from 'mattermost-redux/utils/user_utils';
import AddGroupsToChannelModal from 'components/add_groups_to_channel_modal';
import ChannelBookmarksSubmenu from 'components/channel_bookmarks_sub_menu';
import ChannelGroupsManageModal from 'components/channel_groups_manage_modal';
import ChannelInviteModal from 'components/channel_invite_modal';
import ChannelMoveToSubMenuOld from 'components/channel_move_to_sub_menu_old';
import ChannelNotificationsModal from 'components/channel_notifications_modal';
import ConvertChannelModal from 'components/convert_channel_modal';
import ConvertGmToChannelModal from 'components/convert_gm_to_channel_modal';
import DeleteChannelModal from 'components/delete_channel_modal';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
import MoreDirectChannels from 'components/more_direct_channels';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import RenameChannelModal from 'components/rename_channel_modal';
import UnarchiveChannelModal from 'components/unarchive_channel_modal';
import Menu from 'components/widgets/menu/menu';
import MobileChannelHeaderPlug from 'plugins/mobile_channel_header_plug';
import {Constants, ModalIdentifiers} from 'utils/constants';
import {localizeMessage} from 'utils/utils';
import type {ChannelHeaderAction} from 'types/store/plugins';
import MenuItemCloseChannel from './menu_items/close_channel';
import MenuItemCloseMessage from './menu_items/close_message';
import MenuItemLeaveChannel from './menu_items/leave_channel';
import MenuItemOpenMembersRHS from './menu_items/open_members_rhs';
import MenuItemToggleFavoriteChannel from './menu_items/toggle_favorite_channel';
import MenuItemToggleInfo from './menu_items/toggle_info';
import MenuItemToggleMuteChannel from './menu_items/toggle_mute_channel';
import MenuItemViewPinnedPosts from './menu_items/view_pinned_posts';
export type Props = {
user: UserProfile;
channel?: Channel;
isDefault: boolean;
isFavorite: boolean;
isReadonly: boolean;
isMuted: boolean;
isArchived: boolean;
isMobile: boolean;
penultimateViewedChannelName: string;
pluginMenuItems: ChannelHeaderAction[];
isLicensedForLDAPGroups: boolean;
isChannelBookmarksEnabled: boolean;
}
export default class ChannelHeaderDropdown extends React.PureComponent<Props> {
render() {
const {
user,
channel,
isDefault,
isFavorite,
isMuted,
isReadonly,
isArchived,
isMobile,
penultimateViewedChannelName,
isLicensedForLDAPGroups,
isChannelBookmarksEnabled,
} = this.props;
if (!channel) {
return null;
}
const isPrivate = channel.type === Constants.PRIVATE_CHANNEL;
const isGroupConstrained = channel.group_constrained === true;
const channelMembersPermission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS : Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS;
const channelPropertiesPermission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES;
const channelDeletePermission = isPrivate ? Permissions.DELETE_PRIVATE_CHANNEL : Permissions.DELETE_PUBLIC_CHANNEL;
const channelUnarchivePermission = Permissions.MANAGE_TEAM;
let divider;
if (isMobile) {
divider = (
<li className='MenuGroup mobile-menu-divider'>
<hr/>
</li>
);
}
const pluginItems = this.props.pluginMenuItems.map((item) => {
return (
<Menu.ItemAction
id={item.id + '_pluginmenuitem'}
key={item.id + '_pluginmenuitem'}
onClick={() => {
if (item.action) {
item.action(channel.id);
}
}}
text={item.text}
/>
);
});
return (
<>
<MenuItemToggleInfo
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
channel={channel}
/>
{/* Remove when this components is migrated to new menus */}
<ChannelMoveToSubMenuOld
channel={channel}
openUp={false}
inHeaderDropdown={true}
/>
<Menu.Group divider={divider}>
<MenuItemToggleFavoriteChannel
show={isMobile}
channel={channel}
isFavorite={isFavorite}
/>
<MenuItemViewPinnedPosts
show={isMobile}
channel={channel}
/>
<Menu.ItemToggleModalRedux
id='channelNotificationPreferences'
show={channel.type !== Constants.DM_CHANNEL && !isArchived}
modalId={ModalIdentifiers.CHANNEL_NOTIFICATIONS}
dialogType={ChannelNotificationsModal}
dialogProps={{
channel,
currentUser: user,
focusOriginElement: 'channelHeaderMenuButton',
}}
text={localizeMessage({id: 'navbar.preferences', defaultMessage: 'Notification Preferences'})}
/>
<MenuItemToggleMuteChannel
id='channelToggleMuteChannel'
user={user}
channel={channel}
isMuted={isMuted}
/>
</Menu.Group>
<Menu.Group divider={divider}>
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelMembersPermission]}
>
<Menu.ItemToggleModalRedux
id='channelInviteMembers'
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault && !isGroupConstrained}
modalId={ModalIdentifiers.CHANNEL_INVITE}
dialogType={ChannelInviteModal}
dialogProps={{channel}}
text={localizeMessage({id: 'navbar.addMembers', defaultMessage: 'Add Members'})}
/>
<Menu.ItemToggleModalRedux
id='channelAddMembers'
show={channel.type === Constants.GM_CHANNEL && !isArchived && !isGroupConstrained}
modalId={ModalIdentifiers.CREATE_DM_CHANNEL}
dialogType={MoreDirectChannels}
dialogProps={{isExistingChannel: true, focusOriginElement: 'channelHeaderMenuButton'}}
text={localizeMessage({id: 'navbar.addMembers', defaultMessage: 'Add Members'})}
/>
</ChannelPermissionGate>
<MenuItemOpenMembersRHS
id='channelViewMembers'
channel={channel}
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && (isArchived || isDefault)}
text={localizeMessage({id: 'channel_header.viewMembers', defaultMessage: 'View Members'})}
/>
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelMembersPermission]}
>
<Menu.ItemToggleModalRedux
id='channelAddGroups'
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault && isGroupConstrained && isLicensedForLDAPGroups}
modalId={ModalIdentifiers.ADD_GROUPS_TO_CHANNEL}
dialogType={AddGroupsToChannelModal}
text={localizeMessage({id: 'navbar.addGroups', defaultMessage: 'Add Groups'})}
/>
<Menu.ItemToggleModalRedux
id='channelManageGroups'
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault && isGroupConstrained && isLicensedForLDAPGroups}
modalId={ModalIdentifiers.MANAGE_CHANNEL_GROUPS}
dialogType={ChannelGroupsManageModal}
dialogProps={{channelID: channel.id}}
text={localizeMessage({id: 'navbar_dropdown.manageGroups', defaultMessage: 'Manage Groups'})}
/>
<MenuItemOpenMembersRHS
id='channelManageMembers'
channel={channel}
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault}
text={localizeMessage({id: 'channel_header.manageMembers', defaultMessage: 'Manage Members'})}
editMembers={!isArchived}
/>
</ChannelPermissionGate>
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelMembersPermission]}
invert={true}
>
<MenuItemOpenMembersRHS
id='channelViewMembers'
channel={channel}
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault}
text={localizeMessage({id: 'channel_header.viewMembers', defaultMessage: 'View Members'})}
/>
</ChannelPermissionGate>
</Menu.Group>
<Menu.Group divider={divider}>
<Menu.ItemToggleModalRedux
id='channelEditHeader'
show={(channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) && !isArchived && !isReadonly}
modalId={ModalIdentifiers.EDIT_CHANNEL_HEADER}
dialogType={EditChannelHeaderModal}
dialogProps={{channel}}
text={localizeMessage({id: 'channel_header.setConversationHeader', defaultMessage: 'Edit Conversation Header'})}
/>
<Menu.ItemToggleModalRedux
id='convertGMPrivateChannel'
show={channel.type === Constants.GM_CHANNEL && !isArchived && !isReadonly && !isGuest(user.roles)}
modalId={ModalIdentifiers.CONVERT_GM_TO_CHANNEL}
dialogType={ConvertGmToChannelModal}
dialogProps={{channel}}
text={localizeMessage({id: 'sidebar_left.sidebar_channel_menu_convert_to_channel', defaultMessage: 'Convert to Private Channel'})}
/>
</Menu.Group>
<Menu.Group divider={divider}>
{isChannelBookmarksEnabled && <ChannelBookmarksSubmenu channel={channel}/>}
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelPropertiesPermission]}
>
<Menu.ItemToggleModalRedux
id='channelEditHeader'
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isReadonly}
modalId={ModalIdentifiers.EDIT_CHANNEL_HEADER}
dialogType={EditChannelHeaderModal}
dialogProps={{channel}}
text={localizeMessage({id: 'channel_header.setHeader', defaultMessage: 'Edit Channel Header'})}
/>
<Menu.ItemToggleModalRedux
id='channelEditPurpose'
show={!isArchived && !isReadonly && channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
modalId={ModalIdentifiers.EDIT_CHANNEL_PURPOSE}
dialogType={EditChannelPurposeModal}
dialogProps={{channel}}
text={localizeMessage({id: 'channel_header.setPurpose', defaultMessage: 'Edit Channel Purpose'})}
/>
<Menu.ItemToggleModalRedux
id='channelRename'
show={!isArchived && channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
modalId={ModalIdentifiers.RENAME_CHANNEL}
dialogType={RenameChannelModal}
dialogProps={{channel}}
text={localizeMessage({id: 'channel_header.rename', defaultMessage: 'Rename Channel'})}
/>
</ChannelPermissionGate>
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[Permissions.CONVERT_PUBLIC_CHANNEL_TO_PRIVATE]}
>
<Menu.ItemToggleModalRedux
id='channelConvertToPrivate'
show={!isArchived && !isDefault && channel.type === Constants.OPEN_CHANNEL}
modalId={ModalIdentifiers.CONVERT_CHANNEL}
dialogType={ConvertChannelModal}
dialogProps={{
channelId: channel.id,
channelDisplayName: channel.display_name,
}}
text={localizeMessage({id: 'channel_header.convert', defaultMessage: 'Convert to Private Channel'})}
/>
</ChannelPermissionGate>
<MenuItemLeaveChannel
id='channelLeaveChannel'
channel={channel}
isDefault={isDefault}
isGuestUser={isGuest(user.roles)}
/>
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelDeletePermission]}
>
<Menu.ItemToggleModalRedux
id='channelArchiveChannel'
show={!isArchived && !isDefault && channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
modalId={ModalIdentifiers.DELETE_CHANNEL}
className='MenuItem__dangerous'
dialogType={DeleteChannelModal}
dialogProps={{
channel,
penultimateViewedChannelName,
}}
text={localizeMessage({id: 'channel_header.delete', defaultMessage: 'Archive Channel'})}
/>
</ChannelPermissionGate>
{isMobile &&
<MobileChannelHeaderPlug
channel={channel}
isDropdown={true}
/>}
<MenuItemCloseMessage
id='channelCloseMessage'
channel={channel}
currentUser={user}
/>
<MenuItemCloseChannel
isArchived={isArchived}
/>
</Menu.Group>
<Menu.Group>
{pluginItems}
</Menu.Group>
<Menu.Group divider={divider}>
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelUnarchivePermission]}
>
<Menu.ItemToggleModalRedux
id='channelUnarchiveChannel'
show={isArchived && !isDefault && channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
modalId={ModalIdentifiers.UNARCHIVE_CHANNEL}
dialogType={UnarchiveChannelModal}
dialogProps={{
channel,
}}
text={localizeMessage({id: 'channel_header.unarchive', defaultMessage: 'Unarchive Channel'})}
/>
</ChannelPermissionGate>
</Menu.Group>
</>
);
}
}

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

@@ -1,102 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {
getCurrentChannel,
isCurrentChannelDefault,
isCurrentChannelFavorite,
isCurrentChannelMuted,
isCurrentChannelArchived,
getRedirectChannelNameForTeam,
} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
getUser,
getCurrentUser,
getUserStatuses,
getCurrentUserId,
} from 'mattermost-redux/selectors/entities/users';
import {getPenultimateViewedChannelName} from 'selectors/local_storage';
import {getChannelHeaderMenuPluginComponents} from 'selectors/plugins';
import {getIsChannelBookmarksEnabled} from 'components/channel_bookmarks/utils';
import {Constants} from 'utils/constants';
import * as Utils from 'utils/utils';
import type {GlobalState} from 'types/store';
import Desktop from './channel_header_dropdown';
import Items from './channel_header_dropdown_items';
import Mobile from './mobile_channel_header_dropdown';
const getTeammateId = createSelector(
'getTeammateId',
getCurrentChannel,
getCurrentUserId,
(channel, currentUserId) => {
if (channel?.type !== Constants.DM_CHANNEL) {
return null;
}
return Utils.getUserIdFromChannelId(channel.name, currentUserId);
},
);
const getTeammateStatus = createSelector(
'getTeammateStatus',
getUserStatuses,
getTeammateId,
(userStatuses, teammateId) => {
if (!teammateId) {
return undefined;
}
return userStatuses[teammateId];
},
);
const mapStateToProps = (state: GlobalState) => ({
user: getCurrentUser(state),
channel: getCurrentChannel(state),
isDefault: isCurrentChannelDefault(state),
isFavorite: isCurrentChannelFavorite(state),
isMuted: isCurrentChannelMuted(state),
isReadonly: false,
isArchived: isCurrentChannelArchived(state),
penultimateViewedChannelName: getPenultimateViewedChannelName(state) || getRedirectChannelNameForTeam(state, getCurrentTeamId(state)),
pluginMenuItems: getChannelHeaderMenuPluginComponents(state),
isLicensedForLDAPGroups: state.entities.general.license.LDAPGroups === 'true',
isChannelBookmarksEnabled: getIsChannelBookmarksEnabled(state),
});
const mobileMapStateToProps = (state: GlobalState) => {
const user = getCurrentUser(state);
const channel = getCurrentChannel(state);
const teammateId = getTeammateId(state);
let teammateIsBot = false;
let displayName = '';
if (teammateId) {
const teammate = getUser(state, teammateId);
teammateIsBot = teammate && teammate.is_bot;
displayName = Utils.getDisplayNameByUser(state, teammate);
}
return {
user,
channel,
teammateId,
teammateIsBot,
teammateStatus: getTeammateStatus(state),
displayName,
};
};
export const ChannelHeaderDropdown = Desktop;
export const ChannelHeaderDropdownItems = connect(mapStateToProps)(Items);
export const MobileChannelHeaderDropdown = connect(mobileMapStateToProps)(Mobile);

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

@@ -1,17 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/ChannelHeaderDropdown/MenuItem.CloseChannel shoud be hidden if the channel is not archived 1`] = `
<MenuItemAction
onClick={[MockFunction]}
show={false}
text="Close Channel"
/>
`;
exports[`components/ChannelHeaderDropdown/MenuItem.CloseChannel should match snapshot 1`] = `
<MenuItemAction
onClick={[MockFunction]}
show={true}
text="Close Channel"
/>
`;

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

@@ -1,45 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import Menu from 'components/widgets/menu/menu';
import CloseChannel from './close_channel';
describe('components/ChannelHeaderDropdown/MenuItem.CloseChannel', () => {
const baseProps = {
isArchived: true,
actions: {
goToLastViewedChannel: jest.fn(),
},
};
it('should match snapshot', () => {
const wrapper = shallow(<CloseChannel {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});
it('shoud be hidden if the channel is not archived', () => {
const props = {
...baseProps,
isArchived: false,
};
const wrapper = shallow(<CloseChannel {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('should runs goToLastViewedChannel function on click', () => {
const props = {
...baseProps,
actions: {
...baseProps.actions,
goToLastViewedChannel: jest.fn(),
},
};
const wrapper = shallow(<CloseChannel {...props}/>);
wrapper.find(Menu.ItemAction).simulate('click');
expect(props.actions.goToLastViewedChannel).toHaveBeenCalled();
});
});

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

@@ -1,34 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import Menu from 'components/widgets/menu/menu';
type Props = {
isArchived: boolean;
actions: {
goToLastViewedChannel: () => void;
};
}
const CloseChannel = ({
isArchived,
actions,
}: Props): JSX.Element => {
const intl = useIntl();
return (
<Menu.ItemAction
show={isArchived}
onClick={actions.goToLastViewedChannel}
text={intl.formatMessage({
id: 'center_panel.archived.closeChannel',
defaultMessage: 'Close Channel',
})}
/>
);
};
export default React.memo(CloseChannel);

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

@@ -1,18 +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 {goToLastViewedChannel} from 'actions/views/channel';
import CloseChannel from './close_channel';
const mapDispatchToProps = (dispatch: Dispatch) => ({
actions: bindActionCreators({
goToLastViewedChannel,
}, dispatch),
});
export default connect(null, mapDispatchToProps)(CloseChannel);

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

@@ -1,17 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/ChannelHeaderDropdown/MenuItem.CloseMessage should match snapshot for DM Channel 1`] = `
<MenuItemAction
onClick={[Function]}
show={true}
text="Close Direct Message"
/>
`;
exports[`components/ChannelHeaderDropdown/MenuItem.CloseMessage should match snapshot for GM Channel 1`] = `
<MenuItemAction
onClick={[Function]}
show={true}
text="Close Group Message"
/>
`;

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

@@ -1,74 +0,0 @@
// 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 {ChannelType} from '@mattermost/types/channels';
import type {TeamType} from '@mattermost/types/teams';
import Menu from 'components/widgets/menu/menu';
import {Constants} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import CloseMessage from './close_message';
describe('components/ChannelHeaderDropdown/MenuItem.CloseMessage', () => {
const baseProps = {
currentUser: TestHelper.getUserMock(),
redirectChannel: 'test-default-channel',
currentTeam: TestHelper.getTeamMock({
id: 'team_id',
name: 'test-team',
display_name: 'Test team display name',
description: 'Test team description',
type: 'team-type' as TeamType,
}),
actions: {
savePreferences: jest.fn(() => Promise.resolve()),
leaveDirectChannel: jest.fn(() => Promise.resolve()),
},
};
const groupChannel = TestHelper.getChannelMock({
id: 'channel_id',
type: Constants.GM_CHANNEL as ChannelType,
});
const directChannel = TestHelper.getChannelMock({
id: 'channel_id',
type: Constants.DM_CHANNEL as ChannelType,
teammate_id: 'teammate-id',
});
it('should match snapshot for DM Channel', () => {
const props = {...baseProps, channel: directChannel};
const wrapper = shallow(<CloseMessage {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('should match snapshot for GM Channel', () => {
const props = {...baseProps, channel: groupChannel};
const wrapper = shallow(<CloseMessage {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('should run savePreferences function on click for DM', () => {
const props = {...baseProps, channel: directChannel};
const wrapper = shallow(<CloseMessage {...props}/>);
wrapper.find(Menu.ItemAction).simulate('click', {
preventDefault: jest.fn(),
});
expect(props.actions.savePreferences).toBeCalledWith(props.currentUser.id, [{user_id: props.currentUser.id, category: Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, name: props.channel.teammate_id, value: 'false'}]);
});
it('should run savePreferences function on click for GM', () => {
const props = {...baseProps, channel: groupChannel};
const wrapper = shallow(<CloseMessage {...props}/>);
wrapper.find(Menu.ItemAction).simulate('click', {
preventDefault: jest.fn(),
});
expect(props.actions.savePreferences).toBeCalledWith(props.currentUser.id, [{user_id: props.currentUser.id, category: Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW, name: props.channel.id, value: 'false'}]);
});
});

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

@@ -1,113 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {Channel} from '@mattermost/types/channels';
import type {PreferenceType} from '@mattermost/types/preferences';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
import Menu from 'components/widgets/menu/menu';
import {getHistory} from 'utils/browser_history';
import {Constants} from 'utils/constants';
import {localizeMessage} from 'utils/utils';
type Props = {
/**
* Object with info about currentUser
*/
currentUser: UserProfile;
/**
* Object with info about currentTeam
*/
currentTeam?: Team;
/**
* String with info about redirect
*/
redirectChannel: string;
/**
* Object with info about channel
*/
channel: Channel;
/**
* Use for test selector
*/
id?: string;
/**
* Object with action creators
*/
actions: {
/**
* Action creator to update user preferences
*/
savePreferences: (userId: string, preferences: PreferenceType[]) => void;
/**
* Action creator to leave DM/GM
*/
leaveDirectChannel: (channelName: string) => void;
};
};
export default class CloseMessage extends React.PureComponent<Props> {
handleClose = (e: React.MouseEvent): void => {
e.preventDefault();
const {
channel,
currentUser,
currentTeam,
redirectChannel,
actions: {
savePreferences,
leaveDirectChannel,
},
} = this.props;
let name: string;
let category;
if (channel.type === Constants.DM_CHANNEL) {
category = Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW;
name = channel.teammate_id!;
} else {
category = Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW;
name = channel.id;
}
leaveDirectChannel(channel.name);
savePreferences(currentUser.id, [{user_id: currentUser.id, category, name, value: 'false'}]);
if (currentTeam) {
getHistory().push(`/${currentTeam.name}/channels/${redirectChannel}`);
}
};
render(): React.ReactNode {
const {id, channel} = this.props;
let text;
if (channel.type === Constants.DM_CHANNEL) {
text = localizeMessage({id: 'center_panel.direct.closeDirectMessage', defaultMessage: 'Close Direct Message'});
} else if (channel.type === Constants.GM_CHANNEL) {
text = localizeMessage({id: 'center_panel.direct.closeGroupMessage', defaultMessage: 'Close Group Message'});
}
return (
<Menu.ItemAction
id={id}
show={channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL}
onClick={this.handleClose}
text={text}
/>
);
}
}

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

@@ -1,29 +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 {savePreferences} from 'mattermost-redux/actions/preferences';
import {getRedirectChannelNameForTeam} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeam, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {leaveDirectChannel} from 'actions/views/channel';
import type {GlobalState} from 'types/store';
import CloseMessage from './close_message';
const mapStateToProps = (state: GlobalState) => {
return {
currentTeam: getCurrentTeam(state),
redirectChannel: getRedirectChannelNameForTeam(state, getCurrentTeamId(state)),
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
actions: bindActionCreators({savePreferences, leaveDirectChannel}, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(CloseMessage);

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

@@ -1,37 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should be hidden if the channel is default channel 1`] = `
<MenuItemAction
isDangerous={true}
onClick={[Function]}
show={false}
text="Leave Channel"
/>
`;
exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should be hidden if the channel type is DM or GM 1`] = `
<MenuItemAction
isDangerous={true}
onClick={[Function]}
show={false}
text="Leave Channel"
/>
`;
exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should be hidden if the channel type is DM or GM 2`] = `
<MenuItemAction
isDangerous={true}
onClick={[Function]}
show={false}
text="Leave Channel"
/>
`;
exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should match snapshot 1`] = `
<MenuItemAction
isDangerous={true}
onClick={[Function]}
show={true}
text="Leave Channel"
/>
`;

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

@@ -1,27 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import type {ConnectedProps} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
import {leaveChannel} from 'actions/views/channel';
import {openModal} from 'actions/views/modals';
import LeaveChannel from './leave_channel';
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators({
leaveChannel,
openModal,
}, dispatch),
};
}
const connector = connect(null, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
export default connector(LeaveChannel);

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

@@ -1,86 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import Menu from 'components/widgets/menu/menu';
import {ModalIdentifiers} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import LeaveChannel from './leave_channel';
describe('components/ChannelHeaderDropdown/MenuItem.LeaveChannel', () => {
const baseProps = {
channel: TestHelper.getChannelMock({
id: 'channel_id',
type: 'O',
}),
isGuestUser: false,
isDefault: false,
actions: {
leaveChannel: jest.fn(),
openModal: jest.fn(),
},
};
it('should match snapshot', () => {
const wrapper = shallow(<LeaveChannel {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});
it('should be hidden if the channel is default channel', () => {
const props = {
...baseProps,
isDefault: true,
};
const wrapper = shallow(<LeaveChannel {...props}/>);
expect(wrapper).toMatchSnapshot();
});
it('should be hidden if the channel type is DM or GM', () => {
const props = {
...baseProps,
channel: {...baseProps.channel},
};
const makeWrapper = () => shallow(<LeaveChannel {...props}/>);
props.channel.type = 'D';
expect(makeWrapper()).toMatchSnapshot();
props.channel.type = 'G';
expect(makeWrapper()).toMatchSnapshot();
});
it('should runs leaveChannel function on click only if the channel is not private', () => {
const props = {
...baseProps,
channel: {...baseProps.channel},
actions: {...baseProps.actions},
};
const wrapper = shallow(<LeaveChannel {...props}/>);
wrapper.find(Menu.ItemAction).simulate('click', {
preventDefault: jest.fn(),
});
expect(props.actions.leaveChannel).toHaveBeenCalledWith(props.channel.id);
expect(props.actions.openModal).not.toHaveBeenCalled();
props.channel.type = 'P';
props.actions.leaveChannel = jest.fn();
wrapper.find(Menu.ItemAction).simulate('click', {
preventDefault: jest.fn(),
});
expect(props.actions.leaveChannel).not.toHaveBeenCalled();
expect(props.actions.openModal).toHaveBeenCalledWith(
expect.objectContaining({
modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL,
dialogProps: {
channel: props.channel,
},
}));
});
});

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

@@ -1,78 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import {useIntl} from 'react-intl';
import type {Channel} from '@mattermost/types/channels';
import LeaveChannelModal from 'components/leave_channel_modal';
import Menu from 'components/widgets/menu/menu';
import {Constants, ModalIdentifiers} from 'utils/constants';
import type {PropsFromRedux} from './index';
type Props = PropsFromRedux & {
/**
* Object with info about user
*/
channel: Channel;
/**
* Boolean whether the channel is default
*/
isDefault: boolean;
/**
* Boolean whether the user is a guest or no
*/
isGuestUser: boolean;
/**
* Use for test selector
*/
id?: string;
};
const LeaveChannel = ({
isDefault = true,
isGuestUser = false,
channel,
actions: {
leaveChannel,
openModal,
},
id,
}: Props) => {
const intl = useIntl();
const handleLeave = useCallback((e: Event) => {
e.preventDefault();
if (channel.type === Constants.PRIVATE_CHANNEL) {
openModal({
modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL,
dialogType: LeaveChannelModal,
dialogProps: {
channel,
},
});
} else {
leaveChannel(channel.id);
}
}, [channel, leaveChannel, openModal]);
return (
<Menu.ItemAction
id={id}
show={(!isDefault || isGuestUser) && channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
onClick={handleLeave}
text={intl.formatMessage({id: 'channel_header.leave', defaultMessage: 'Leave Channel'})}
isDangerous={true}
/>
);
};
export default memo(LeaveChannel);

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

@@ -1,27 +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 {showChannelMembers} from 'actions/views/rhs';
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
import {RHSStates} from 'utils/constants';
import type {GlobalState} from 'types/store';
import OpenChannelMembersRHS from './open_members_rhs';
const mapStateToProps = (state: GlobalState) => ({
rhsOpen: getIsRhsOpen(state) && getRhsState(state) === RHSStates.CHANNEL_MEMBERS,
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
actions: bindActionCreators({
showChannelMembers,
}, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(OpenChannelMembersRHS);

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

@@ -1,53 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {Channel} from '@mattermost/types/channels';
import Menu from 'components/widgets/menu/menu';
type Action = {
showChannelMembers: (channelId: string, editMembers: boolean) => void;
};
type OwnProps = {
channel: Channel;
show: boolean;
id: string;
editMembers?: boolean;
text: string;
}
type Props = {
rhsOpen: boolean;
actions: Action;
} & OwnProps;
const ToggleChannelMembersRHS = ({
show,
id,
channel,
rhsOpen,
text,
editMembers = false,
actions,
}: Props) => {
const openRHSIfNotOpen = () => {
if (rhsOpen) {
return;
}
actions.showChannelMembers(channel.id, editMembers);
};
return (
<Menu.ItemAction
show={show}
id={id}
onClick={openRHSIfNotOpen}
text={text}
/>
);
};
export default ToggleChannelMembersRHS;

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

@@ -1,17 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel should match snapshot for favorite channel 1`] = `
<MenuItemAction
onClick={[Function]}
show={true}
text="Remove from Favorites"
/>
`;
exports[`components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel should match snapshot for not favorite channel 1`] = `
<MenuItemAction
onClick={[Function]}
show={true}
text="Add to Favorites"
/>
`;

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

@@ -1,19 +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 {favoriteChannel, unfavoriteChannel} from 'mattermost-redux/actions/channels';
import ToggleFavoriteChannel from './toggle_favorite_channel';
const mapDispatchToProps = (dispatch: Dispatch) => ({
actions: bindActionCreators({
favoriteChannel,
unfavoriteChannel,
}, dispatch),
});
export default connect(null, mapDispatchToProps)(ToggleFavoriteChannel);

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

@@ -1,88 +0,0 @@
// 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 {ChannelType} from '@mattermost/types/channels';
import Menu from 'components/widgets/menu/menu';
import ToggleFavoriteChannel from './toggle_favorite_channel';
describe('components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel', () => {
const baseProps = {
channel: {
id: 'channel_id',
display_name: 'channel_display_name',
create_at: 0,
update_at: 0,
delete_at: 0,
team_id: '',
type: 'O' as ChannelType,
name: '',
header: '',
purpose: '',
last_post_at: 0,
last_root_post_at: 0,
creator_id: '',
scheme_id: '',
group_constrained: false,
},
actions: {
favoriteChannel: jest.fn(),
unfavoriteChannel: jest.fn(),
},
};
const propsForFavorite = {
...baseProps,
isFavorite: true,
actions: {
favoriteChannel: jest.fn(),
unfavoriteChannel: jest.fn(),
},
};
const propsForNotFavorite = {
...baseProps,
isFavorite: false,
actions: {
favoriteChannel: jest.fn(),
unfavoriteChannel: jest.fn(),
},
};
it('should match snapshot for favorite channel', () => {
const wrapper = shallow(<ToggleFavoriteChannel {...propsForFavorite}/>);
expect(wrapper).toMatchSnapshot();
});
it('should runs unfavoriteChannel function for favorite channel', () => {
const wrapper = shallow(<ToggleFavoriteChannel {...propsForFavorite}/>);
wrapper.find(Menu.ItemAction).simulate('click', {
preventDefault: jest.fn(),
});
expect(propsForFavorite.actions.unfavoriteChannel).toHaveBeenCalledWith(propsForFavorite.channel.id);
expect(propsForFavorite.actions.favoriteChannel).not.toHaveBeenCalled();
});
it('should match snapshot for not favorite channel', () => {
const wrapper = shallow(<ToggleFavoriteChannel {...propsForNotFavorite}/>);
expect(wrapper).toMatchSnapshot();
});
it('should runs favoriteChannel function for not favorite channel', () => {
const wrapper = shallow(<ToggleFavoriteChannel {...propsForNotFavorite}/>);
wrapper.find(Menu.ItemAction).simulate('click', {
preventDefault: jest.fn(),
});
expect(propsForNotFavorite.actions.favoriteChannel).toHaveBeenCalledWith(propsForFavorite.channel.id);
expect(propsForNotFavorite.actions.unfavoriteChannel).not.toHaveBeenCalled();
});
});

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

@@ -1,59 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo, useCallback} from 'react';
import type {MouseEvent} from 'react';
import {useIntl} from 'react-intl';
import type {Channel} from '@mattermost/types/channels';
import Menu from 'components/widgets/menu/menu';
type Action = {
favoriteChannel: (channelId: string) => void;
unfavoriteChannel: (channelId: string) => void;
};
type Props = {
show?: boolean;
channel: Channel;
isFavorite: boolean;
actions: Action;
};
const ToggleFavoriteChannel = ({
show = true,
isFavorite,
actions: {
favoriteChannel,
unfavoriteChannel,
},
channel,
}: Props) => {
const intl = useIntl();
const toggleFavoriteChannel = useCallback((channelId: string) => {
return isFavorite ? unfavoriteChannel(channelId) : favoriteChannel(channelId);
}, [isFavorite, favoriteChannel, unfavoriteChannel]);
const handleClick = useCallback((e: MouseEvent<HTMLButtonElement>): void => {
e.preventDefault();
toggleFavoriteChannel(channel.id);
}, [channel.id, toggleFavoriteChannel]);
let text;
if (isFavorite) {
text = intl.formatMessage({id: 'channelHeader.removeFromFavorites', defaultMessage: 'Remove from Favorites'});
} else {
text = intl.formatMessage({id: 'channelHeader.addToFavorites', defaultMessage: 'Add to Favorites'});
}
return (
<Menu.ItemAction
show={show}
onClick={handleClick}
text={text}
/>
);
};
export default memo(ToggleFavoriteChannel);

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

@@ -1,28 +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 {closeRightHandSide, showChannelInfo} from 'actions/views/rhs';
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
import {RHSStates} from 'utils/constants';
import type {GlobalState} from 'types/store';
import ToggleInfo from './toggle_info';
const mapStateToProps = (state: GlobalState) => ({
rhsOpen: getIsRhsOpen(state) && getRhsState(state) === RHSStates.CHANNEL_INFO,
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
actions: bindActionCreators({
closeRightHandSide,
showChannelInfo,
}, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(ToggleInfo);

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

@@ -1,50 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import type {Channel} from '@mattermost/types/channels';
import Menu from 'components/widgets/menu/menu';
type Action = {
closeRightHandSide: () => void;
showChannelInfo: (channelId: string) => void;
};
type Props = {
show: boolean;
channel: Channel;
rhsOpen: boolean;
actions: Action;
};
const ToggleInfo = ({show, channel, rhsOpen, actions}: Props) => {
const intl = useIntl();
const toggleRHS = () => {
if (rhsOpen) {
actions.closeRightHandSide();
return;
}
actions.showChannelInfo(channel.id);
};
let text;
if (rhsOpen) {
text = intl.formatMessage({id: 'channelHeader.hideInfo', defaultMessage: 'Close Info'});
} else {
text = intl.formatMessage({id: 'channelHeader.viewInfo', defaultMessage: 'View Info'});
}
return (
<Menu.ItemAction
show={show}
onClick={toggleRHS}
text={text}
/>
);
};
export default ToggleInfo;

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

@@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/ChannelHeaderDropdown/MenuItemToggleMuteChannel should match snapshot 1`] = `
<MenuItemAction
onClick={[Function]}
show={true}
text="Mute Channel"
/>
`;

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

@@ -1,18 +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 {updateChannelNotifyProps} from 'mattermost-redux/actions/channels';
import MenuItemToggleMuteChannel from './toggle_mute_channel';
const mapDispatchToProps = (dispatch: Dispatch) => ({
actions: bindActionCreators({
updateChannelNotifyProps,
}, dispatch),
});
export default connect(null, mapDispatchToProps)(MenuItemToggleMuteChannel);

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

@@ -1,116 +0,0 @@
// 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 {Channel} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import Menu from 'components/widgets/menu/menu';
import MenuItemAction from 'components/widgets/menu/menu_items/menu_item_action';
import {Constants, NotificationLevels} from 'utils/constants';
import MenuItemToggleMuteChannel from './toggle_mute_channel';
describe('components/ChannelHeaderDropdown/MenuItemToggleMuteChannel', () => {
const baseProps = {
user: {
id: 'user_id',
} as UserProfile,
channel: {
id: 'channel_id',
type: 'O',
} as Channel,
isMuted: false,
actions: {
updateChannelNotifyProps: jest.fn(),
},
};
it('should match snapshot', () => {
const wrapper = shallow(<MenuItemToggleMuteChannel {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});
it('should unmute channel on click the channel was muted', () => {
const props = {
...baseProps,
isMuted: true,
actions: {
updateChannelNotifyProps: jest.fn(),
},
};
const wrapper = shallow(<MenuItemToggleMuteChannel {...props}/>);
wrapper.find(Menu.ItemAction).simulate('click');
expect(props.actions.updateChannelNotifyProps).toBeCalledWith(
props.user.id,
props.channel.id,
{mark_unread: NotificationLevels.ALL},
);
});
it('should mute channel on click the channel was unmuted', () => {
const props = {
...baseProps,
isMuted: false,
actions: {
updateChannelNotifyProps: jest.fn(),
},
};
const wrapper = shallow(<MenuItemToggleMuteChannel {...props}/>);
wrapper.find(Menu.ItemAction).simulate('click');
expect(props.actions.updateChannelNotifyProps).toBeCalledWith(
props.user.id,
props.channel.id,
{mark_unread: NotificationLevels.MENTION},
);
});
it('should show Mute Channel to all channel types except DM_CHANNEL and GM_CHANNEL', () => {
[
Constants.OPEN_CHANNEL,
Constants.PRIVATE_CHANNEL,
Constants.ARCHIVED_CHANNEL,
].forEach((channelType) => {
const channel = {
id: 'channel_id',
type: channelType,
} as Channel;
const wrapper = shallow(
<MenuItemToggleMuteChannel
{...baseProps}
channel={channel}
/>,
);
expect(wrapper.find(MenuItemAction).props().show).toEqual(true);
expect(wrapper.find(MenuItemAction).props().text).toEqual('Mute Channel');
});
});
it('should show Mute Conversation to channel types DM_CHANNEL and GM_CHANNEL', () => {
[
Constants.DM_CHANNEL,
Constants.GM_CHANNEL,
].forEach((channelType) => {
const channel = {
id: 'channel_id',
type: channelType,
} as Channel;
const wrapper = shallow(
<MenuItemToggleMuteChannel
{...baseProps}
channel={channel}
/>,
);
expect(wrapper.find(MenuItemAction).props().show).toEqual(true);
expect(wrapper.find(MenuItemAction).props().text).toEqual('Mute Conversation');
});
});
});

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

@@ -1,79 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import type {Channel, ChannelNotifyProps} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import Menu from 'components/widgets/menu/menu';
import {Constants, NotificationLevels} from 'utils/constants';
export type Actions = {
updateChannelNotifyProps(userId: string, channelId: string, props: Partial<ChannelNotifyProps>): void;
};
type Props = {
/**
* Object with info about the current user
*/
user: UserProfile;
/**
* Object with info about the current channel
*/
channel: Channel;
/**
* Boolean whether the current channel is muted
*/
isMuted: boolean;
/**
* Use for test selector
*/
id?: string;
/**
* Object with action creators
*/
actions: Actions;
};
export default function MenuItemToggleMuteChannel({
id,
isMuted,
channel,
user,
actions,
}: Props) {
const intl = useIntl();
const handleClick = useCallback(() => {
actions.updateChannelNotifyProps(user.id, channel.id, {
mark_unread: (isMuted ? NotificationLevels.ALL : NotificationLevels.MENTION) as 'all' | 'mention',
});
}, [actions, isMuted, user.id, channel.id]);
let text;
if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) {
text = isMuted ?
intl.formatMessage({id: 'channel_header.unmuteConversation', defaultMessage: 'Unmute Conversation'}) :
intl.formatMessage({id: 'channel_header.muteConversation', defaultMessage: 'Mute Conversation'});
} else {
text = isMuted ?
intl.formatMessage({id: 'channel_header.unmute', defaultMessage: 'Unmute Channel'}) :
intl.formatMessage({id: 'channel_header.mute', defaultMessage: 'Mute Channel'});
}
return (
<Menu.ItemAction
id={id}
onClick={handleClick}
text={text}
/>
);
}

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

@@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/ChannelHeaderDropdown/MenuItem.ViewPinnedPosts should match snapshot 1`] = `
<MenuItemAction
onClick={[Function]}
show={true}
text="View Pinned Posts"
/>
`;

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

@@ -1,28 +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 {closeRightHandSide, showPinnedPosts} from 'actions/views/rhs';
import {getRhsState} from 'selectors/rhs';
import {RHSStates} from 'utils/constants';
import type {GlobalState} from 'types/store';
import ViewPinnedPosts from './view_pinned_posts';
const mapStateToProps = (state: GlobalState) => ({
hasPinnedPosts: getRhsState(state) === RHSStates.PIN,
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
actions: bindActionCreators({
closeRightHandSide,
showPinnedPosts,
}, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(ViewPinnedPosts);

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

@@ -1,51 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import Menu from 'components/widgets/menu/menu';
import ViewPinnedPosts from './view_pinned_posts';
describe('components/ChannelHeaderDropdown/MenuItem.ViewPinnedPosts', () => {
const baseProps = {
channel: {
id: 'channel_id',
},
hasPinnedPosts: true,
actions: {
closeRightHandSide: jest.fn(),
showPinnedPosts: jest.fn(),
},
};
it('should match snapshot', () => {
const wrapper = shallow(<ViewPinnedPosts {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});
it('should runs closeRightHandSide function if has any pinned posts', () => {
const wrapper = shallow(<ViewPinnedPosts {...baseProps}/>);
wrapper.find(Menu.ItemAction).simulate('click', {
preventDefault: jest.fn(),
});
expect(baseProps.actions.closeRightHandSide).toHaveBeenCalled();
});
it('should runs showPinnedPosts function if has not pinned posts', () => {
const props = {
...baseProps,
hasPinnedPosts: false,
};
const wrapper = shallow(<ViewPinnedPosts {...props}/>);
wrapper.find(Menu.ItemAction).simulate('click', {
preventDefault: jest.fn(),
});
expect(baseProps.actions.showPinnedPosts).toHaveBeenCalled();
});
});

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

@@ -1,49 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, memo} from 'react';
import type {MouseEvent} from 'react';
import {useIntl} from 'react-intl';
import Menu from 'components/widgets/menu/menu';
type Props = {
show?: boolean;
channel: any;
hasPinnedPosts: boolean;
actions: {
closeRightHandSide: () => void;
showPinnedPosts: (id: any) => void;
};
}
const ViewPinnedPosts = ({
channel,
hasPinnedPosts,
actions: {
closeRightHandSide,
showPinnedPosts,
},
show,
}: Props) => {
const intl = useIntl();
const handleClick = useCallback((e: MouseEvent) => {
e.preventDefault();
if (hasPinnedPosts) {
closeRightHandSide();
} else {
showPinnedPosts(channel.id);
}
}, [channel.id, closeRightHandSide, showPinnedPosts, hasPinnedPosts]);
return (
<Menu.ItemAction
show={show}
onClick={handleClick}
text={intl.formatMessage({id: 'navbar.viewPinnedPosts', defaultMessage: 'View Pinned Posts'})}
/>
);
};
export default memo(ViewPinnedPosts);

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

@@ -1,90 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import type {Channel} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import {ChannelHeaderDropdownItems} from 'components/channel_header_dropdown';
import StatusIcon from 'components/status_icon';
import Menu from 'components/widgets/menu/menu';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import {Constants} from 'utils/constants';
import MobileChannelHeaderDropdownAnimation from './mobile_channel_header_dropdown_animation';
type Props = {
user: UserProfile;
channel?: Channel;
teammateId: string | null;
teammateIsBot?: boolean;
teammateStatus?: string;
displayName: string;
}
const MobileChannelHeaderDropdown = ({
user,
channel,
teammateId,
displayName,
teammateIsBot,
teammateStatus,
}: Props) => {
const intl = useIntl();
const getChannelTitle = () => {
if (!channel) {
return '';
}
if (channel.type === Constants.DM_CHANNEL) {
if (user.id === teammateId) {
return (
<FormattedMessage
id='channel_header.directchannel.you'
defaultMessage='{displayname} (you)'
values={{displayname: displayName}}
/>
);
}
return displayName;
}
return channel.display_name;
};
let dmHeaderIconStatus;
if (!teammateIsBot) {
dmHeaderIconStatus = (
<StatusIcon status={teammateStatus}/>
);
}
return (
<MenuWrapper animationComponent={MobileChannelHeaderDropdownAnimation}>
<a>
<span className='heading'>
{dmHeaderIconStatus}
{getChannelTitle()}
</span>
<span
className='fa fa-angle-down header-dropdown__icon'
title={intl.formatMessage({id: 'generic_icons.dropdown', defaultMessage: 'Dropdown Icon'})}
/>
</a>
<Menu ariaLabel={intl.formatMessage({id: 'channel_header.menuAriaLabel', defaultMessage: 'Channel Menu'})}>
<ChannelHeaderDropdownItems isMobile={true}/>
<div className='Menu__close visible-xs-block'>
{'×'}
</div>
</Menu>
</MenuWrapper>
);
};
export default memo(MobileChannelHeaderDropdown);

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

@@ -1,36 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {ReactNode} from 'react';
import {CSSTransition} from 'react-transition-group';
const ANIMATION_DURATION = 350;
type Props = {
children?: ReactNode;
show: boolean;
};
const timeout = {
enter: ANIMATION_DURATION,
exit: ANIMATION_DURATION,
};
const MobileChannelHeaderDropdownAnimation = ({show, children}: Props) => {
return (
<CSSTransition
in={show}
classNames='mobile-channel-header-dropdown'
enter={true}
exit={true}
mountOnEnter={true}
unmountOnExit={true}
timeout={timeout}
>
{children}
</CSSTransition>
);
};
export default React.memo(MobileChannelHeaderDropdownAnimation);

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

@@ -0,0 +1,181 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import type {ReactNode} from 'react';
import React from 'react';
import {useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import ChevronDownIcon from '@mattermost/compass-icons/components/chevron-down';
import type {UserProfile} from '@mattermost/types/users';
import {
getCurrentChannel,
isCurrentChannelDefault,
isCurrentChannelFavorite,
isCurrentChannelMuted,
} from 'mattermost-redux/selectors/entities/channels';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {
getCurrentUser,
} from 'mattermost-redux/selectors/entities/users';
import {getChannelHeaderMenuPluginComponents} from 'selectors/plugins';
import * as Menu from 'components/menu';
import {Constants} from 'utils/constants';
import ChannelDirectMenu from './channel_header_menu_items/channel_header_direct_menu';
import ChannelGroupMenu from './channel_header_menu_items/channel_header_group_menu';
import ChannelHeaderMobileMenu from './channel_header_menu_items/channel_header_mobile_menu';
import ChannelPublicPrivateMenu from './channel_header_menu_items/channel_header_public_private_menu';
import ChannelHeaderTitleDirect from '../channel_header/channel_header_title_direct';
import ChannelHeaderTitleGroup from '../channel_header/channel_header_title_group';
type Props = {
dmUser?: UserProfile;
gmMembers?: UserProfile[];
archivedIcon?: JSX.Element;
sharedIcon?: JSX.Element;
isMobile?: boolean;
}
export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archivedIcon, sharedIcon}: Props): JSX.Element | null {
const intl = useIntl();
const user = useSelector(getCurrentUser);
const channel = useSelector(getCurrentChannel);
const isDefault = useSelector(isCurrentChannelDefault);
const isFavorite = useSelector(isCurrentChannelFavorite);
const isMuted = useSelector(isCurrentChannelMuted);
const isLicensedForLDAPGroups = useSelector(getLicense).LDAPGroups === 'true';
const pluginMenuItems = useSelector(getChannelHeaderMenuPluginComponents);
const isReadonly = false;
if (!channel) {
return null;
}
const isDirect = (channel.type === Constants.DM_CHANNEL);
const isGroup = (channel.type === Constants.GM_CHANNEL);
let channelTitle: ReactNode = channel.display_name;
let ariaLabel = intl.formatMessage({
id: 'channel_header.otherchannel',
defaultMessage: '{displayName} Channel Menu',
}, {
displayName: channel.display_name,
});
if (isDirect && dmUser) {
channelTitle = <ChannelHeaderTitleDirect dmUser={dmUser}/>;
if (user.id === dmUser.id) {
ariaLabel = intl.formatMessage({
id: 'channel_header.directchannel',
defaultMessage: '{displayName} (you) Channel Menu',
}, {
displayName: channel.display_name,
});
}
} else if (isGroup) {
channelTitle = <ChannelHeaderTitleGroup gmMembers={gmMembers}/>;
}
const pluginItems = pluginMenuItems.map((item) => {
const handlePluginItemClick = () => {
if (item.action) {
item.action(channel.id);
}
};
return (
<Menu.Item
id={item.id + '_pluginmenuitem'}
key={item.id + '_pluginmenuitem'}
onClick={handlePluginItemClick}
labels={<span>{item.text}</span>}
/>
);
});
return (
<Menu.Container
menuButtonTooltip={{
text: channelTitle as string,
}}
menuButton={{
id: 'channelHeaderDropdownButton',
class: classNames('channel-header__trigger style--none'),
children: (
<>
{archivedIcon}
<strong
id='channelHeaderTitle'
className='heading'
>
{channelTitle as string}
</strong>
{sharedIcon}
<ChevronDownIcon size={16}/>
</>
),
'aria-label': ariaLabel.toLowerCase(),
}}
menu={{
id: 'channelHeaderDropdownMenu',
}}
transformOrigin={{
horizontal: 'left',
vertical: 'top',
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
{isDirect && (
<ChannelDirectMenu
channel={channel}
user={user}
isMuted={isMuted}
pluginItems={pluginItems}
isFavorite={isFavorite}
isMobile={isMobile || false}
/>
)}
{isGroup && (
<ChannelGroupMenu
channel={channel}
user={user}
isMuted={isMuted}
pluginItems={pluginItems}
isFavorite={isFavorite}
isMobile={isMobile || false}
/>
)}
{(!isDirect && !isGroup) && (
<ChannelPublicPrivateMenu
channel={channel}
user={user}
isMuted={isMuted}
pluginItems={pluginItems}
isFavorite={isFavorite}
isMobile={isMobile || false}
isDefault={isDefault}
isReadonly={isReadonly}
isLicensedForLDAPGroups={isLicensedForLDAPGroups}
/>
)}
<ChannelHeaderMobileMenu
isMobile={isMobile || false}
pluginItems={pluginItems}
channel={channel}
/>
</Menu.Container>
);
}

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

@@ -0,0 +1,74 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ReactNode} from 'react';
import React from 'react';
import {CogOutlineIcon} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import ChannelMoveToSubMenu from 'components/channel_move_to_sub_menu';
import * as Menu from 'components/menu';
import CloseMessage from '../menu_items/close_message';
import EditConversationHeader from '../menu_items/edit_conversation_header';
import MenuItemPluginItems from '../menu_items/plugins_submenu';
import MenuItemToggleFavoriteChannel from '../menu_items/toggle_favorite_channel';
import MenuItemToggleInfo from '../menu_items/toggle_info';
import MenuItemToggleMuteChannel from '../menu_items/toggle_mute_channel';
import MenuItemViewPinnedPosts from '../menu_items/view_pinned_posts';
interface Props extends Menu.FirstMenuItemProps {
channel: Channel;
user: UserProfile;
isMuted: boolean;
isMobile: boolean;
isFavorite: boolean;
pluginItems: ReactNode[];
}
const ChannelHeaderDirectMenu = ({channel, user, isMuted, isMobile, isFavorite, pluginItems, ...rest}: Props) => {
return (
<>
<MenuItemToggleInfo
channel={channel}
{...rest}
/>
<MenuItemToggleMuteChannel
userID={user.id}
channel={channel}
isMuted={isMuted}
/>
{isMobile && (
<>
<MenuItemToggleFavoriteChannel
channelID={channel.id}
isFavorite={isFavorite}
/>
<MenuItemViewPinnedPosts
channelID={channel.id}
/>
</>
)}
<EditConversationHeader
leadingElement={<CogOutlineIcon size='18px'/>}
channel={channel}
/>
<Menu.Separator/>
<ChannelMoveToSubMenu
channel={channel}
/>
{!isMobile && (
<MenuItemPluginItems pluginItems={pluginItems}/>
)}
<Menu.Separator/>
<CloseMessage
currentUserID={user.id}
channel={channel}
/>
</>
);
};
export default ChannelHeaderDirectMenu;

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

@@ -0,0 +1,136 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ReactNode} from 'react';
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {
ChevronRightIcon,
CogOutlineIcon,
} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import {Permissions} from 'mattermost-redux/constants';
import {isGuest} from 'mattermost-redux/utils/user_utils';
import ChannelMoveToSubMenu from 'components/channel_move_to_sub_menu';
import * as Menu from 'components/menu';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import CloseMessage from '../menu_items/close_message';
import MenuItemConvertToPrivate from '../menu_items/convert_gm_to_private';
import EditConversationHeader from '../menu_items/edit_conversation_header';
import MenuItemNotification from '../menu_items/notification';
import MenuItemOpenMembersRHS from '../menu_items/open_members_rhs';
import MenuItemPluginItems from '../menu_items/plugins_submenu';
import MenuItemToggleFavoriteChannel from '../menu_items/toggle_favorite_channel';
import MenuItemToggleInfo from '../menu_items/toggle_info';
import MenuItemToggleMuteChannel from '../menu_items/toggle_mute_channel';
import MenuItemViewPinnedPosts from '../menu_items/view_pinned_posts';
interface Props extends Menu.FirstMenuItemProps {
channel: Channel;
user: UserProfile;
isMuted: boolean;
isMobile: boolean;
isFavorite: boolean;
pluginItems: ReactNode[];
}
const ChannelHeaderGroupMenu = ({channel, user, isMuted, isMobile, isFavorite, pluginItems, ...rest}: Props) => {
const isGroupConstrained = channel?.group_constrained === true;
const isArchived = channel.delete_at !== 0;
const {formatMessage} = useIntl();
return (
<>
<MenuItemToggleInfo
channel={channel}
{...rest}
/>
<MenuItemToggleMuteChannel
userID={user.id}
channel={channel}
isMuted={isMuted}
/>
{isMobile && (
<>
<MenuItemToggleFavoriteChannel
channelID={channel.id}
isFavorite={isFavorite}
/>
<MenuItemViewPinnedPosts
channelID={channel.id}
/>
</>
)}
{!isArchived && (
<MenuItemNotification
user={user}
channel={channel}
/>
)}
{(isArchived && isGroupConstrained && isGuest(user.roles)) && (
<EditConversationHeader
leadingElement={<CogOutlineIcon size='18px'/>}
channel={channel}
/>
)}
{(!isArchived && !isGroupConstrained && !isGuest(user.roles)) && (
<Menu.SubMenu
id={'channelSettings'}
labels={
<FormattedMessage
id='channel_header.settings'
defaultMessage='Settings'
/>
}
leadingElement={<CogOutlineIcon size={18}/>}
trailingElements={<ChevronRightIcon size={16}/>}
menuId={'channelSettings-menu'}
menuAriaLabel={formatMessage({id: 'channel_header.settings', defaultMessage: 'Settings'})}
>
<EditConversationHeader
channel={channel}
/>
<MenuItemConvertToPrivate
channel={channel}
/>
</Menu.SubMenu>
)}
<Menu.Separator/>
{(!isArchived && !isGroupConstrained) && (
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS]}
>
<MenuItemOpenMembersRHS
id='channelMembers'
channel={channel}
text={
<FormattedMessage
id='channel_header.members'
defaultMessage='Members'
/>
}
/>
<Menu.Separator/>
</ChannelPermissionGate>
)}
<ChannelMoveToSubMenu channel={channel}/>
{!isMobile && (
<MenuItemPluginItems pluginItems={pluginItems}/>
)}
<Menu.Separator/>
<CloseMessage
currentUserID={user.id}
channel={channel}
/>
</>
);
};
export default ChannelHeaderGroupMenu;

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

@@ -0,0 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ReactNode} from 'react';
import React, {memo} from 'react';
import type {Channel} from '@mattermost/types/channels';
import * as Menu from 'components/menu';
import MobileChannelHeaderPlugins from '../menu_items/mobile_channel_header_plugins';
type Props = {
isMobile: boolean;
channel: Channel;
pluginItems: ReactNode[];
}
const ChannelHeaderMobileMenu = (props: Props): JSX.Element => {
if (!props.isMobile) {
return <></>;
}
return (
<>
<MobileChannelHeaderPlugins
channel={props.channel}
isDropdown={true}
/>
<Menu.Separator/>
{props.pluginItems}
</>
);
};
export default memo(ChannelHeaderMobileMenu);

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

@@ -0,0 +1,200 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ReactNode} from 'react';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import type {Channel} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import {Permissions} from 'mattermost-redux/constants';
import {isGuest} from 'mattermost-redux/utils/user_utils';
import ChannelMoveToSubMenu from 'components/channel_move_to_sub_menu';
import * as Menu from 'components/menu';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import {Constants} from 'utils/constants';
import MenuItemArchiveChannel from '../menu_items/archive_channel';
import MenuItemChannelBookmarks from '../menu_items/channel_bookmarks_submenu';
import MenuItemChannelSettings from '../menu_items/channel_settings_submenu';
import MenuItemCloseChannel from '../menu_items/close_channel';
import MenuItemGroupsMenuItems from '../menu_items/groups';
import MenuItemLeaveChannel from '../menu_items/leave_channel';
import MenuItemNotification from '../menu_items/notification';
import MenuItemOpenMembersRHS from '../menu_items/open_members_rhs';
import MenuItemPluginItems from '../menu_items/plugins_submenu';
import MenuItemToggleFavoriteChannel from '../menu_items/toggle_favorite_channel';
import MenuItemToggleInfo from '../menu_items/toggle_info';
import MenuItemToggleMuteChannel from '../menu_items/toggle_mute_channel';
import MenuItemUnarchiveChannel from '../menu_items/unarchive_channel';
import MenuItemViewPinnedPosts from '../menu_items/view_pinned_posts';
interface Props extends Menu.FirstMenuItemProps {
channel: Channel;
user: UserProfile;
isMuted: boolean;
isReadonly: boolean;
isDefault: boolean;
isMobile: boolean;
isFavorite: boolean;
isLicensedForLDAPGroups: boolean;
pluginItems: ReactNode[];
}
const ChannelHeaderPublicMenu = ({channel, user, isMuted, isReadonly, isDefault, isMobile, isFavorite, isLicensedForLDAPGroups, pluginItems, ...rest}: Props) => {
const isGroupConstrained = channel?.group_constrained === true;
const isArchived = channel.delete_at !== 0;
const isPrivate = channel?.type === Constants.PRIVATE_CHANNEL;
const channelMembersPermission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS : Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS;
const channelDeletePermission = isPrivate ? Permissions.DELETE_PRIVATE_CHANNEL : Permissions.DELETE_PUBLIC_CHANNEL;
const channelUnarchivePermission = Permissions.MANAGE_TEAM;
return (
<>
<MenuItemToggleInfo
channel={channel}
{...rest}
/>
<MenuItemToggleMuteChannel
userID={user.id}
channel={channel}
isMuted={isMuted}
/>
{!isArchived && (
<>
<MenuItemNotification
user={user}
channel={channel}
/>
<MenuItemChannelSettings
isReadonly={isReadonly}
isDefault={isDefault}
channel={channel}
/>
<MenuItemChannelBookmarks
channel={channel}
/>
</>
)}
<Menu.Separator/>
{isMobile && (
<>
<MenuItemToggleFavoriteChannel
channelID={channel.id}
isFavorite={isFavorite}
/>
<MenuItemViewPinnedPosts
channelID={channel.id}
/>
<Menu.Separator/>
</>
)}
{(isArchived || isDefault) && (
<MenuItemOpenMembersRHS
id='channelMembers'
channel={channel}
text={
<FormattedMessage
id='channel_header.members'
defaultMessage='Members'
/>
}
/>
)}
{!isArchived && !isDefault && (
<>
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelMembersPermission]}
>
{isGroupConstrained && isLicensedForLDAPGroups && (
<MenuItemGroupsMenuItems
channel={channel}
/>
)}
<MenuItemOpenMembersRHS
id='channelMembers'
channel={channel}
text={
<FormattedMessage
id='channel_header.members'
defaultMessage='Members'
/>
}
/>
</ChannelPermissionGate>
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelMembersPermission]}
invert={true}
>
<MenuItemOpenMembersRHS
id='channelMembers'
channel={channel}
text={
<FormattedMessage
id='channel_header.members'
defaultMessage='Members'
/>
}
/>
</ChannelPermissionGate>
</>
)}
<Menu.Separator/>
<ChannelMoveToSubMenu channel={channel}/>
{!isMobile && (
<MenuItemPluginItems pluginItems={pluginItems}/>
)}
{!isDefault && (
<Menu.Separator/>
)}
{!isDefault && !isGuest(user.roles) && (
<MenuItemLeaveChannel
id='channelLeaveChannel'
channel={channel}
/>
)}
{isArchived && (
<MenuItemCloseChannel/>
)}
{!isArchived && !isDefault && (
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelDeletePermission]}
>
<MenuItemArchiveChannel
channel={channel}
/>
</ChannelPermissionGate>
)}
{isArchived && !isDefault && (
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelUnarchivePermission]}
>
<MenuItemUnarchiveChannel
channel={channel}
/>
</ChannelPermissionGate>
)}
</>
);
};
export default ChannelHeaderPublicMenu;

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

@@ -0,0 +1,64 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useDispatch} from 'react-redux';
import * as modalActions from 'actions/views/modals';
import ChannelInviteModal from 'components/channel_invite_modal';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import {ModalIdentifiers} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import AddChannelMembers from './add_channel_members';
describe('components/ChannelHeaderMenu/MenuItems/AddChannelMembers', () => {
const channel = TestHelper.getChannelMock({header: 'Test Header'});
beforeEach(() => {
jest.spyOn(modalActions, 'openModal');
// Mock useDispatch to return our custom dispatch function
jest.spyOn(require('react-redux'), 'useDispatch');
});
afterEach(() => {
jest.clearAllMocks();
});
test('renders the component correctly', () => {
renderWithContext(
<AddChannelMembers
channel={channel}
/>, {},
);
const menuItem = screen.getByText('Add Members');
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
});
test('dispatches openModal action on click', () => {
renderWithContext(
<WithTestMenuContext>
<AddChannelMembers
channel={channel}
/>
</WithTestMenuContext>, {},
);
const menuItem = screen.getByText('Add Members');
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
fireEvent.click(menuItem); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
expect(modalActions.openModal).toHaveBeenCalledWith({
modalId: ModalIdentifiers.CHANNEL_INVITE,
dialogType: ChannelInviteModal,
dialogProps: {channel},
});
});
});

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

@@ -0,0 +1,48 @@
// 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 {useDispatch} from 'react-redux';
import {AccountPlusOutlineIcon} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import {openModal} from 'actions/views/modals';
import ChannelInviteModal from 'components/channel_invite_modal';
import * as Menu from 'components/menu';
import {ModalIdentifiers} from 'utils/constants';
type Props = {
channel: Channel;
}
const AddChannelMembers = ({channel}: Props): JSX.Element => {
const dispatch = useDispatch();
const handleAddMembers = () => {
dispatch(openModal({
modalId: ModalIdentifiers.CHANNEL_INVITE,
dialogType: ChannelInviteModal,
dialogProps: {channel},
}));
};
return (
<Menu.Item
id='channelAddMembers'
leadingElement={<AccountPlusOutlineIcon size='18px'/>}
onClick={handleAddMembers}
labels={
<FormattedMessage
id='navbar.addMembers'
defaultMessage='Add Members'
/>
}
/>
);
};
export default React.memo(AddChannelMembers);

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

@@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useDispatch} from 'react-redux';
import * as modalActions from 'actions/views/modals';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import MoreDirectChannels from 'components/more_direct_channels';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import {ModalIdentifiers} from 'utils/constants';
import AddGroupMembers from './add_group_members';
describe('components/ChannelHeaderMenu/MenuItems/AddGroupMembers', () => {
beforeEach(() => {
jest.spyOn(modalActions, 'openModal');
// Mock useDispatch to return our custom dispatch function
jest.spyOn(require('react-redux'), 'useDispatch');
});
afterEach(() => {
jest.clearAllMocks();
});
test('renders the component correctly', () => {
renderWithContext(
<AddGroupMembers/>, {},
);
const menuItem = screen.getByText('Add Members');
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
});
test('dispatches openModal action on click', () => {
renderWithContext(
<WithTestMenuContext>
<AddGroupMembers/>
</WithTestMenuContext>, {},
);
const menuItem = screen.getByText('Add Members');
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
fireEvent.click(menuItem); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
expect(modalActions.openModal).toHaveBeenCalledWith({
modalId: ModalIdentifiers.CREATE_DM_CHANNEL,
dialogType: MoreDirectChannels,
dialogProps: {
focusOriginElement: 'channelHeaderDropdownButton',
isExistingChannel: true,
},
});
});
});

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

@@ -0,0 +1,45 @@
// 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 {useDispatch} from 'react-redux';
import {AccountMultipleOutlineIcon} from '@mattermost/compass-icons/components';
import {openModal} from 'actions/views/modals';
import * as Menu from 'components/menu';
import MoreDirectChannels from 'components/more_direct_channels';
import {ModalIdentifiers} from 'utils/constants';
const AddGroupMembers = (): JSX.Element => {
const dispatch = useDispatch();
const handleAddGroupMembers = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.CREATE_DM_CHANNEL,
dialogType: MoreDirectChannels,
dialogProps: {isExistingChannel: true, focusOriginElement: 'channelHeaderDropdownButton'},
}),
);
};
return (
<Menu.Item
id='channelAddMembers'
leadingElement={<AccountMultipleOutlineIcon size='18px'/>}
onClick={handleAddGroupMembers}
labels={
<FormattedMessage
id='navbar.addMembers'
defaultMessage='Add Members'
/>
}
/>
);
};
export default React.memo(AddGroupMembers);

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

@@ -0,0 +1,104 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useDispatch} from 'react-redux';
import * as modalActions from 'actions/views/modals';
import LocalStorageStore from 'stores/local_storage_store';
import DeleteChannelModal from 'components/delete_channel_modal';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import {ModalIdentifiers} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import ArchiveChannel from './archive_channel';
describe('components/ChannelHeaderMenu/MenuItems/ArchiveChannel', () => {
const initialState = {
entities: {
channels: {
currentChannelId: 'current_channel_id',
channels: {
current_channel_id: TestHelper.getChannelMock({
id: 'current_channel_id',
name: 'default-name',
display_name: 'Default',
delete_at: 0,
type: 'O',
team_id: 'team_id',
}),
},
},
teams: {
currentTeamId: 'team-id',
teams: {
'team-id': {
id: 'team_id',
name: 'team-1',
display_name: 'Team 1',
},
},
myMembers: {
'team-id': {roles: 'team_role'},
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
locale: 'en',
roles: 'system_role'},
},
},
},
};
LocalStorageStore.setPenultimateChannelName('current_user_id', 'team-id', 'current_channel_id');
const channel = TestHelper.getChannelMock({header: 'Test Header'});
beforeEach(() => {
jest.spyOn(modalActions, 'openModal');
// Mock useDispatch to return our custom dispatch function
jest.spyOn(require('react-redux'), 'useDispatch');
});
afterEach(() => {
jest.clearAllMocks();
});
test('renders the component correctly', () => {
renderWithContext(
<ArchiveChannel channel={channel}/>, initialState,
);
const menuItem = screen.getByText('Archive Channel');
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
});
test('dispatches openModal action on click with default channel', () => {
renderWithContext(
<WithTestMenuContext>
<ArchiveChannel channel={channel}/>
</WithTestMenuContext>, initialState,
);
const menuItem = screen.getByText('Archive Channel');
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
fireEvent.click(menuItem); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
expect(modalActions.openModal).toHaveBeenCalledWith({
modalId: ModalIdentifiers.DELETE_CHANNEL,
dialogType: DeleteChannelModal,
dialogProps: {
channel,
penultimateViewedChannelName: 'current_channel_id',
},
});
});
});

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

@@ -0,0 +1,61 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {ArchiveOutlineIcon} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import {getRedirectChannelNameForCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
import {openModal} from 'actions/views/modals';
import {getPenultimateViewedChannelName} from 'selectors/local_storage';
import DeleteChannelModal from 'components/delete_channel_modal';
import * as Menu from 'components/menu';
import {ModalIdentifiers} from 'utils/constants';
type Props = {
channel: Channel;
}
const ArchiveChannel = ({
channel,
}: Props) => {
const dispatch = useDispatch();
const redirectChannelName = useSelector(getRedirectChannelNameForCurrentTeam);
const penultimateViewedChannelName = useSelector(getPenultimateViewedChannelName) || redirectChannelName;
const handleArchiveChannel = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.DELETE_CHANNEL,
dialogType: DeleteChannelModal,
dialogProps: {
channel,
penultimateViewedChannelName,
},
}),
);
};
return (
<Menu.Item
id='channelArchiveChannel'
leadingElement={<ArchiveOutlineIcon size={18}/>}
onClick={handleArchiveChannel}
labels={
<FormattedMessage
id='channel_header.delete'
defaultMessage='Archive Channel'
/>
}
isDestructive={true}
/>
);
};
export default memo(ArchiveChannel);

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

@@ -0,0 +1,88 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Purpose of this file to exists is only required until channel header dropdown is migrated to new menus
import React, {memo} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import {
ChevronRightIcon,
LinkVariantIcon,
PaperclipIcon,
BookmarkOutlineIcon,
} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import {getChannelBookmarks} from 'mattermost-redux/selectors/entities/channel_bookmarks';
import {useBookmarkAddActions} from 'components/channel_bookmarks/channel_bookmarks_menu';
import {MAX_BOOKMARKS_PER_CHANNEL, useCanUploadFiles, useChannelBookmarkPermission} from 'components/channel_bookmarks/utils';
import * as Menu from 'components/menu';
import type {GlobalState} from 'types/store';
type Props = {
channel: Channel;
};
const ChannelBookmarksSubmenu = (props: Props) => {
const {formatMessage} = useIntl();
const {handleCreateLink, handleCreateFile} = useBookmarkAddActions(props.channel.id);
const canAdd = useChannelBookmarkPermission(props.channel.id, 'add');
const canUploadFiles = useCanUploadFiles();
useSelector((state: GlobalState) => {
const bookmarks = getChannelBookmarks(state, props.channel.id);
return bookmarks && Object.keys(bookmarks).length >= MAX_BOOKMARKS_PER_CHANNEL;
});
if (!canAdd) {
return null;
}
return (
<Menu.SubMenu
id={`channel-menu-${props.channel.id}-bookmarks`}
leadingElement={<BookmarkOutlineIcon size={18}/>}
labels={(
<FormattedMessage
id='channel_menu.bookmarks'
defaultMessage='Bookmarks Bar'
/>
)}
trailingElements={(
<ChevronRightIcon size={16}/>
)}
menuId={`channel-menu-${props.channel.id}-menu`}
menuAriaLabel={formatMessage({id: 'channel_menu.bookmarks', defaultMessage: 'Bookmarks Bar'})}
>
<Menu.Item
id={`channel-menu-${props.channel.id}-bookmarks-link`}
leadingElement={<LinkVariantIcon size={18}/>}
labels={(
<FormattedMessage
id='channel_menu.bookmarks.addLink'
defaultMessage='Add a link'
/>
)}
onClick={() => handleCreateLink()}
/>
{canUploadFiles && (
<Menu.Item
id={`channel-menu-${props.channel.id}-bookmarks-file`}
leadingElement={<PaperclipIcon size={18}/>}
labels={(
<FormattedMessage
id='channel_menu.bookmarks.addFile'
defaultMessage='Attach a file'
/>
)}
onClick={() => handleCreateFile()}
/>
)}
</Menu.SubMenu>
);
};
export default memo(ChannelBookmarksSubmenu);

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

@@ -0,0 +1,167 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch} from 'react-redux';
import {
ChevronRightIcon,
CogOutlineIcon,
} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import {Permissions} from 'mattermost-redux/constants';
import {openModal} from 'actions/views/modals';
import ConvertChannelModal from 'components/convert_channel_modal';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
import * as Menu from 'components/menu';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import RenameChannelModal from 'components/rename_channel_modal';
import {Constants, ModalIdentifiers} from 'utils/constants';
type Props = {
channel: Channel;
isReadonly: boolean;
isDefault: boolean;
}
const ChannelSettingsSubmenu = ({channel, isReadonly, isDefault}: Props): JSX.Element => {
const dispatch = useDispatch();
const {formatMessage} = useIntl();
const channelPropertiesPermission = channel.type === Constants.PRIVATE_CHANNEL ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES;
const handleRenameChannel = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.RENAME_CHANNEL,
dialogType: RenameChannelModal,
dialogProps: {channel},
}),
);
};
const handleEditHeader = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
dialogType: EditChannelHeaderModal,
dialogProps: {channel},
}),
);
};
const handleEditPurpose = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE,
dialogType: EditChannelPurposeModal,
dialogProps: {channel},
}),
);
};
const handleConvertToPrivate = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.CONVERT_CHANNEL,
dialogType: ConvertChannelModal,
dialogProps: {
channelId: channel.id,
channelDisplayName: channel.display_name,
},
}),
);
};
return (
<Menu.SubMenu
id={'channelSettings'}
labels={
<FormattedMessage
id='channelSettings'
defaultMessage='Channel Settings'
/>
}
leadingElement={<CogOutlineIcon size={18}/>}
trailingElements={<ChevronRightIcon size={16}/>}
menuId={'channelSettings-menu'}
menuAriaLabel={formatMessage({id: 'channelSettings', defaultMessage: 'Channel Settings'})}
>
{!isReadonly && (
<Menu.Item
id='channelRename'
onClick={handleRenameChannel}
labels={
<FormattedMessage
id='channel_header.rename'
defaultMessage='Rename Channel'
/>
}
/>
)}
{!isReadonly && (
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelPropertiesPermission]}
>
<Menu.Item
id='channelEditHeader'
onClick={handleEditHeader}
labels={
<FormattedMessage
id='channel_header.setHeader'
defaultMessage='Edit Channel Header'
/>
}
/>
</ChannelPermissionGate>
)}
{!isReadonly && (
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelPropertiesPermission]}
>
<Menu.Item
id='channelEditPurpose'
onClick={handleEditPurpose}
labels={
<FormattedMessage
id='channel_header.setPurpose'
defaultMessage='Edit Channel Purpose'
/>
}
/>
</ChannelPermissionGate>
)}
{!isDefault && channel.type === Constants.OPEN_CHANNEL && (
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[Permissions.CONVERT_PUBLIC_CHANNEL_TO_PRIVATE]}
>
<Menu.Item
id='channelConvertToPrivate'
onClick={handleConvertToPrivate}
labels={
<FormattedMessage
id='channel_header.convert'
defaultMessage='Convert to Private Channel'
/>
}
/>
</ChannelPermissionGate>
)}
</Menu.SubMenu>
);
};
export default memo(ChannelSettingsSubmenu);

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

@@ -0,0 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import * as channelActions from 'actions/views/channel';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import CloseChannel from './close_channel';
describe('components/ChannelHeaderMenu/MenuItems/CloseChannel', () => {
beforeEach(() => {
jest.spyOn(channelActions, 'goToLastViewedChannel');
});
afterEach(() => {
jest.clearAllMocks();
});
test('renders the component correctly, handle click event', () => {
renderWithContext(
<WithTestMenuContext>
<CloseChannel/>
</WithTestMenuContext>, {},
);
const menuItem = screen.getByText('Close Channel');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem); // Simulate click on the menu item
expect(channelActions.goToLastViewedChannel).toHaveBeenCalledTimes(1); // Ensure dispatch was called
});
});

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

@@ -0,0 +1,26 @@
// 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 {goToLastViewedChannel} from 'actions/views/channel';
import * as Menu from 'components/menu';
interface Props extends Menu.FirstMenuItemProps {}
const CloseChannel = ({...rest}: Props): JSX.Element => {
return (
<Menu.Item
onClick={goToLastViewedChannel}
labels={
<FormattedMessage
id='center_panel.archived.closeChannel'
defaultMessage='Close Channel'
/>}
{...rest}
/>
);
};
export default React.memo(CloseChannel);

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

@@ -0,0 +1,130 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import * as preferences from 'mattermost-redux/actions/preferences';
import * as channelActions from 'actions/views/channel';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import {Constants} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import CloseMessage from './close_message';
describe('components/ChannelHeaderMenu/MenuItems/CloseMessage', () => {
const initialState = {
entities: {
channels: {
currentChannelId: 'current_channel_id',
channels: {
current_channel_id: TestHelper.getChannelMock({
id: 'current_channel_id',
name: 'default-name',
display_name: 'Default',
delete_at: 0,
type: 'O',
team_id: 'team_id',
}),
},
},
teams: {
currentTeamId: 'team-id',
teams: {
'team-id': {
id: 'team_id',
name: 'team-1',
display_name: 'Team 1',
},
},
myMembers: {
'team-id': {roles: 'team_role'},
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {
locale: 'en',
roles: 'system_role',
},
},
},
},
};
const groupChannel = TestHelper.getChannelMock({
id: 'channel_id',
name: 'groupChannel',
type: Constants.GM_CHANNEL as ChannelType,
});
const directChannel = TestHelper.getChannelMock({
id: 'channel_id',
type: Constants.DM_CHANNEL as ChannelType,
teammate_id: 'teammate-id',
name: 'directChannel',
});
beforeEach(() => {
jest.spyOn(channelActions, 'leaveDirectChannel');
jest.spyOn(preferences, 'savePreferences');
});
afterEach(() => {
jest.clearAllMocks();
});
test('renders the component correctly for group channel', () => {
renderWithContext(
<WithTestMenuContext>
<CloseMessage
currentUserID='current_user_id'
channel={groupChannel}
/>
</WithTestMenuContext>, initialState,
);
const menuItem = screen.getByText('Close Group Message');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem); // Simulate click on the menu item
expect(channelActions.leaveDirectChannel).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(channelActions.leaveDirectChannel).toHaveBeenCalledWith(groupChannel.name);
expect(preferences.savePreferences).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(preferences.savePreferences).toHaveBeenCalledWith(
'current_user_id',
[{user_id: 'current_user_id', category: Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW, name: groupChannel.id, value: 'false'}],
);
});
test('renders the component correctly for direct channel', () => {
renderWithContext(
<WithTestMenuContext>
<CloseMessage
currentUserID='current_user_id'
channel={directChannel}
/>
</WithTestMenuContext>, initialState,
);
const menuItem = screen.getByText('Close Direct Message');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem); // Simulate click on the menu item
expect(channelActions.leaveDirectChannel).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(channelActions.leaveDirectChannel).toHaveBeenCalledWith(directChannel.name);
expect(preferences.savePreferences).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(preferences.savePreferences).toHaveBeenCalledWith(
'current_user_id',
[{user_id: 'current_user_id', category: Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, name: directChannel.teammate_id, value: 'false'}],
);
});
});

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

@@ -0,0 +1,81 @@
// 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 {useDispatch, useSelector} from 'react-redux';
import {CloseIcon} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getRedirectChannelNameForCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {leaveDirectChannel} from 'actions/views/channel';
import * as Menu from 'components/menu';
import {getHistory} from 'utils/browser_history';
import {Constants} from 'utils/constants';
type Props = {
currentUserID: string;
channel: Channel;
id?: string;
};
export default function CloseMessage(props: Props) {
const dispatch = useDispatch();
const currentTeam = useSelector(getCurrentTeam);
const redirectChannel = useSelector(getRedirectChannelNameForCurrentTeam);
const handleClose = () => {
const {
channel,
currentUserID,
} = props;
let name: string;
let category;
if (channel.type === Constants.DM_CHANNEL) {
category = Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW;
name = channel.teammate_id!;
} else {
category = Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW;
name = channel.id;
}
dispatch(leaveDirectChannel(channel.name));
dispatch(savePreferences(currentUserID, [{user_id: currentUserID, category, name, value: 'false'}]));
if (currentTeam) {
getHistory().push(`/${currentTeam.name}/channels/${redirectChannel}`);
}
};
const {id, channel} = props;
// DM
let text = (
<FormattedMessage
id='center_panel.direct.closeDirectMessage'
defaultMessage='Close Direct Message'
/>);
if (channel.type === Constants.GM_CHANNEL) {
text = (
<FormattedMessage
id='center_panel.direct.closeGroupMessage'
defaultMessage='Close Group Message'
/>);
}
return (
<Menu.Item
id={id}
leadingElement={<CloseIcon size='18px'/>}
onClick={handleClose}
labels={text}
/>
);
}

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

@@ -0,0 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useDispatch} from 'react-redux';
import * as modalActions from 'actions/views/modals';
import ConvertGmToChannelModal from 'components/convert_gm_to_channel_modal';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import {ModalIdentifiers} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import ConvertGMtoPrivate from './convert_gm_to_private';
describe('components/ChannelHeaderMenu/MenuItems/ConvertGMtoPrivate', () => {
beforeEach(() => {
jest.spyOn(modalActions, 'openModal');
// Mock useDispatch to return our custom dispatch function
jest.spyOn(require('react-redux'), 'useDispatch');
});
afterEach(() => {
jest.clearAllMocks();
});
const channel = TestHelper.getChannelMock();
test('renders the component correctly, handle click event', () => {
renderWithContext(
<WithTestMenuContext>
<ConvertGMtoPrivate channel={channel}/>
</WithTestMenuContext>, {},
);
const menuItem = screen.getByText('Convert to Private Channel');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
expect(modalActions.openModal).toHaveBeenCalledWith({
modalId: ModalIdentifiers.CONVERT_GM_TO_CHANNEL,
dialogType: ConvertGmToChannelModal,
dialogProps: {channel},
});
});
});

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

@@ -0,0 +1,47 @@
// 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 {useDispatch} from 'react-redux';
import type {Channel} from '@mattermost/types/channels';
import {openModal} from 'actions/views/modals';
import ConvertGmToChannelModal from 'components/convert_gm_to_channel_modal';
import * as Menu from 'components/menu';
import {ModalIdentifiers} from 'utils/constants';
type Props = {
channel: Channel;
}
const ConvertGMtoPrivate = ({channel}: Props): JSX.Element => {
const dispatch = useDispatch();
const handleConvertToPrivate = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.CONVERT_GM_TO_CHANNEL,
dialogType: ConvertGmToChannelModal,
dialogProps: {channel},
}),
);
};
return (
<Menu.Item
id='convertGMPrivateChannel'
onClick={handleConvertToPrivate}
labels={
<FormattedMessage
id='sidebar_left.sidebar_channel_menu_convert_to_channel'
defaultMessage='Convert to Private Channel'
/>
}
/>
);
};
export default React.memo(ConvertGMtoPrivate);

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

@@ -0,0 +1,53 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useDispatch} from 'react-redux';
import * as modalActions from 'actions/views/modals';
import ConvertChannelModal from 'components/convert_channel_modal';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import {ModalIdentifiers} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import ConvertPublictoPrivate from './convert_public_to_private';
describe('components/ChannelHeaderMenu/MenuItems/ConvertPublicToPrivate', () => {
beforeEach(() => {
jest.spyOn(modalActions, 'openModal');
// Mock useDispatch to return our custom dispatch function
jest.spyOn(require('react-redux'), 'useDispatch');
});
afterEach(() => {
jest.clearAllMocks();
});
const channel = TestHelper.getChannelMock();
test('renders the component correctly, handle click event', () => {
renderWithContext(
<WithTestMenuContext>
<ConvertPublictoPrivate channel={channel}/>
</WithTestMenuContext>, {},
);
const menuItem = screen.getByText('Convert to Private Channel');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
expect(modalActions.openModal).toHaveBeenCalledWith({
modalId: ModalIdentifiers.CONVERT_CHANNEL,
dialogType: ConvertChannelModal,
dialogProps: {
channelId: channel.id,
channelDisplayName: channel.display_name,
},
});
});
});

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

@@ -0,0 +1,50 @@
// 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 {useDispatch} from 'react-redux';
import type {Channel} from '@mattermost/types/channels';
import {openModal} from 'actions/views/modals';
import ConvertChannelModal from 'components/convert_channel_modal';
import * as Menu from 'components/menu';
import {ModalIdentifiers} from 'utils/constants';
type Props = {
channel: Channel;
}
const ConvertPublictoPrivate = ({channel}: Props): JSX.Element => {
const dispatch = useDispatch();
const handleConvertToPrivate = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.CONVERT_CHANNEL,
dialogType: ConvertChannelModal,
dialogProps: {
channelId: channel.id,
channelDisplayName: channel.display_name,
},
}),
);
};
return (
<Menu.Item
id='channelConvertToPrivate'
onClick={handleConvertToPrivate}
labels={
<FormattedMessage
id='channel_header.convert'
defaultMessage='Convert to Private Channel'
/>
}
/>
);
};
export default React.memo(ConvertPublictoPrivate);

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

@@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useDispatch} from 'react-redux';
import * as modalActions from 'actions/views/modals';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import RenameChannelModal from 'components/rename_channel_modal';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import {ModalIdentifiers} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import EditChannelSettings from './edit_channel_settings';
describe('components/ChannelHeaderMenu/MenuItems/EditChannelSettings', () => {
const channel = TestHelper.getChannelMock();
beforeEach(() => {
jest.spyOn(modalActions, 'openModal');
jest.spyOn(require('react-redux'), 'useDispatch');
});
afterEach(() => {
jest.clearAllMocks();
});
test('renders the component correctly, handle click event', () => {
renderWithContext(
<WithTestMenuContext>
<EditChannelSettings
channel={channel}
isReadonly={false}
isDefault={false}
/>
</WithTestMenuContext>, {},
);
const menuItem = screen.getByText('Rename Channel');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
expect(modalActions.openModal).toHaveBeenCalledWith({
modalId: ModalIdentifiers.RENAME_CHANNEL,
dialogType: RenameChannelModal,
dialogProps: {channel},
});
});
});

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

@@ -0,0 +1,121 @@
// 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 {useDispatch} from 'react-redux';
import type {Channel} from '@mattermost/types/channels';
import {Permissions} from 'mattermost-redux/constants';
import {openModal} from 'actions/views/modals';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
import * as Menu from 'components/menu';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import RenameChannelModal from 'components/rename_channel_modal';
import {Constants, ModalIdentifiers} from 'utils/constants';
import MenuItemConvertToPrivate from './convert_public_to_private';
type Props = {
channel: Channel;
isReadonly: boolean;
isDefault: boolean;
}
const EditChannelSettings = ({channel, isReadonly, isDefault}: Props): JSX.Element => {
const dispatch = useDispatch();
const channelPropertiesPermission = channel.type === Constants.PRIVATE_CHANNEL ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES;
const handleRenameChannel = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.RENAME_CHANNEL,
dialogType: RenameChannelModal,
dialogProps: {channel},
}),
);
};
const handleEditHeader = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
dialogType: EditChannelHeaderModal,
dialogProps: {channel},
}),
);
};
const handleEditPurpose = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE,
dialogType: EditChannelPurposeModal,
dialogProps: {channel},
}),
);
};
return (
<>
{!isReadonly && (
<>
<Menu.Item
id='channelRename'
onClick={handleRenameChannel}
labels={
<FormattedMessage
id='channel_header.rename'
defaultMessage='Rename Channel'
/>
}
/>
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[channelPropertiesPermission]}
>
<Menu.Item
id='channelEditHeader'
onClick={handleEditHeader}
labels={
<FormattedMessage
id='channel_header.setHeader'
defaultMessage='Edit Channel Header'
/>
}
/>
<Menu.Item
id='channelEditPurpose'
onClick={handleEditPurpose}
labels={
<FormattedMessage
id='channel_header.setPurpose'
defaultMessage='Edit Channel Purpose'
/>
}
/>
</ChannelPermissionGate>
</>
)}
{!isDefault && channel.type === Constants.OPEN_CHANNEL && (
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}
permissions={[Permissions.CONVERT_PUBLIC_CHANNEL_TO_PRIVATE]}
>
<MenuItemConvertToPrivate
channel={channel}
/>
</ChannelPermissionGate>
)}
</>
);
};
export default React.memo(EditChannelSettings);

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

@@ -0,0 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useDispatch} from 'react-redux';
import * as modalActions from 'actions/views/modals';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import {ModalIdentifiers} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import EditConversationHeader from './edit_conversation_header';
describe('components/ChannelHeaderMenu/MenuItems/EditConversationHeader', () => {
beforeEach(() => {
jest.spyOn(modalActions, 'openModal');
// Mock useDispatch to return our custom dispatch function
jest.spyOn(require('react-redux'), 'useDispatch');
});
afterEach(() => {
jest.clearAllMocks();
});
const channel = TestHelper.getChannelMock();
test('renders the component correctly, handle click event', () => {
renderWithContext(
<WithTestMenuContext>
<EditConversationHeader channel={channel}/>
</WithTestMenuContext>, {},
);
const menuItem = screen.getByText('Edit Header');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
expect(modalActions.openModal).toHaveBeenCalledWith({
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
dialogType: EditChannelHeaderModal,
dialogProps: {channel},
});
});
});

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

@@ -0,0 +1,49 @@
// 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 {useDispatch} from 'react-redux';
import type {Channel} from '@mattermost/types/channels';
import {openModal} from 'actions/views/modals';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import * as Menu from 'components/menu';
import {ModalIdentifiers} from 'utils/constants';
type Props = {
channel: Channel;
leadingElement?: React.ReactNode;
}
const EditConversationHeader = ({channel, leadingElement}: Props): JSX.Element => {
const dispatch = useDispatch();
const handleEditHeader = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
dialogType: EditChannelHeaderModal,
dialogProps: {channel},
}),
);
};
return (
<Menu.Item
id='channelEditHeader'
leadingElement={leadingElement}
onClick={handleEditHeader}
labels={
<FormattedMessage
id='channel_header.setConversationHeader'
defaultMessage='Edit Header'
/>
}
/>
);
};
export default React.memo(EditConversationHeader);

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

@@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useDispatch} from 'react-redux';
import * as modalActions from 'actions/views/modals';
import AddGroupsToChannelModal from 'components/add_groups_to_channel_modal';
import ChannelGroupsManageModal from 'components/channel_groups_manage_modal';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import {ModalIdentifiers} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import Groups from './groups';
describe('components/ChannelHeaderMenu/MenuItems/Groups', () => {
beforeEach(() => {
jest.spyOn(modalActions, 'openModal');
// Mock useDispatch to return our custom dispatch function
jest.spyOn(require('react-redux'), 'useDispatch');
});
afterEach(() => {
jest.clearAllMocks();
});
const channel = TestHelper.getChannelMock();
test('renders the component correctly, handle click event for add groups', () => {
renderWithContext(
<WithTestMenuContext>
<Groups channel={channel}/>
</WithTestMenuContext>, {},
);
const menuItem = screen.getByText('Add Groups');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
expect(modalActions.openModal).toHaveBeenCalledWith({
modalId: ModalIdentifiers.ADD_GROUPS_TO_CHANNEL,
dialogType: AddGroupsToChannelModal,
});
});
test('renders the component correctly, handle click event for manage groups', () => {
renderWithContext(
<WithTestMenuContext>
<Groups channel={channel}/>
</WithTestMenuContext>, {},
);
const menuItemMG = screen.getByText('Manage Groups');
expect(menuItemMG).toBeInTheDocument();
fireEvent.click(menuItemMG); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
expect(modalActions.openModal).toHaveBeenCalledWith({
modalId: ModalIdentifiers.MANAGE_CHANNEL_GROUPS,
dialogType: ChannelGroupsManageModal,
dialogProps: {channelID: channel.id},
});
});
});

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

@@ -0,0 +1,74 @@
// 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 {useDispatch} from 'react-redux';
import {AccountMultipleOutlineIcon, AccountMultiplePlusOutlineIcon} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import {openModal} from 'actions/views/modals';
import AddGroupsToChannelModal from 'components/add_groups_to_channel_modal';
import ChannelGroupsManageModal from 'components/channel_groups_manage_modal';
import * as Menu from 'components/menu';
import {ModalIdentifiers} from 'utils/constants';
interface Props extends Menu.FirstMenuItemProps {
channel: Channel;
}
const Groups = ({channel, ...rest}: Props): JSX.Element => {
const dispatch = useDispatch();
const handleAddGroups = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.ADD_GROUPS_TO_CHANNEL,
dialogType: AddGroupsToChannelModal,
}),
);
};
const handleManageGroups = () => {
dispatch(
openModal({
modalId: ModalIdentifiers.MANAGE_CHANNEL_GROUPS,
dialogType: ChannelGroupsManageModal,
dialogProps: {channelID: channel.id},
}),
);
};
return (
<>
<Menu.Item
id='channelAddGroups'
leadingElement={<AccountMultiplePlusOutlineIcon size='18px'/>}
onClick={handleAddGroups}
labels={
<FormattedMessage
id='navbar.addGroups'
defaultMessage='Add Groups'
/>
}
{...rest}
/>
<Menu.Item
id='channelManageGroups'
leadingElement={<AccountMultipleOutlineIcon size='18px'/>}
onClick={handleManageGroups}
labels={
<FormattedMessage
id='navbar_dropdown.manageGroups'
defaultMessage='Manage Groups'
/>
}
{...rest}
/>
</>
);
};
export default React.memo(Groups);

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

@@ -0,0 +1,73 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {useDispatch} from 'react-redux';
import * as channelActions from 'actions/views/channel';
import * as modalActions from 'actions/views/modals';
import LeaveChannelModal from 'components/leave_channel_modal';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
import {ModalIdentifiers} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import LeaveChannel from './leave_channel';
describe('components/ChannelHeaderMenu/MenuItems/LeaveChannelTest', () => {
beforeEach(() => {
jest.spyOn(modalActions, 'openModal');
jest.spyOn(channelActions, 'leaveChannel');
// Mock useDispatch to return our custom dispatch function
jest.spyOn(require('react-redux'), 'useDispatch');
});
afterEach(() => {
jest.clearAllMocks();
});
test('renders the component correctly, handle click event correctly for Public Channel', () => {
const channel = TestHelper.getChannelMock();
renderWithContext(
<WithTestMenuContext>
<LeaveChannel channel={channel}/>
</WithTestMenuContext>, {},
);
const menuItem = screen.getByText('Leave Channel');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(channelActions.leaveChannel).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(channelActions.leaveChannel).toHaveBeenCalledWith(channel.id);
});
test('renders the component correctly, handle click event for manage groups', () => {
const channel = TestHelper.getChannelMock({type: 'P'});
renderWithContext(
<WithTestMenuContext>
<LeaveChannel channel={channel}/>
</WithTestMenuContext>, {},
);
const menuItemMG = screen.getByText('Leave Channel');
expect(menuItemMG).toBeInTheDocument();
fireEvent.click(menuItemMG); // Simulate click on the menu item
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
expect(modalActions.openModal).toHaveBeenCalledWith({
modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL,
dialogType: LeaveChannelModal,
dialogProps: {
channel,
},
});
});
});

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

@@ -0,0 +1,63 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {memo} from 'react';
import {FormattedMessage} from 'react-intl';
import {useDispatch} from 'react-redux';
import {LogoutVariantIcon} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import {leaveChannel} from 'actions/views/channel';
import {openModal} from 'actions/views/modals';
import LeaveChannelModal from 'components/leave_channel_modal';
import * as Menu from 'components/menu';
import {Constants, ModalIdentifiers} from 'utils/constants';
// import type {PropsFromRedux} from './index';
type Props = {
channel: Channel;
id?: string;
}
const LeaveChannel = ({
channel,
id,
}: Props) => {
const dispatch = useDispatch();
const handleLeave = () => {
if (channel.type === Constants.PRIVATE_CHANNEL) {
dispatch(
openModal({
modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL,
dialogType: LeaveChannelModal,
dialogProps: {
channel,
},
}),
);
} else {
dispatch(leaveChannel(channel.id));
}
};
return (
<Menu.Item
id={id}
leadingElement={<LogoutVariantIcon size='18px'/>}
onClick={handleLeave}
labels={
<FormattedMessage
id='channel_header.leave'
defaultMessage='Leave Channel'
/>
}
isDestructive={true}
/>
);
};
export default memo(LeaveChannel);

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

@@ -0,0 +1,369 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import cloneDeep from 'lodash/cloneDeep';
import React from 'react';
import {useDispatch} from 'react-redux';
import {AppBindingLocations, AppCallResponseTypes} from 'mattermost-redux/constants/apps';
import * as appsActions from 'actions/apps';
import * as channelActions from 'actions/views/channel';
import * as modalActions from 'actions/views/modals';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, fireEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import MobileChannelHeaderPlugins from './mobile_channel_header_plugins';
describe('components/ChannelHeaderMenu/MenuItems/MobileChannelHeaderPlugins, with no extended components', () => {
jest.mock('actions/apps', () => ({
...jest.requireActual('actions/apps'),
handleBindingClick: jest.fn(),
}));
beforeEach(() => {
jest.spyOn(modalActions, 'openModal');
jest.spyOn(channelActions, 'leaveChannel');
// jest.spyOn(appsActions, 'handleBindingClick');
jest.spyOn(appsActions, 'openAppsModal');
jest.spyOn(appsActions, 'postEphemeralCallResponseForChannel');
// Mock useDispatch to return our custom dispatch function
jest.spyOn(require('react-redux'), 'useDispatch');
});
afterEach(() => {
jest.clearAllMocks();
});
const channel = TestHelper.getChannelMock();
const action = jest.fn();
const pluginState = {
plugins: {
components: {
MobileChannelHeaderButton: [
{
id: 'someid',
pluginId: 'pluginid',
icon: <i className='fa fa-anchor'/>,
action,
dropdownText: 'some dropdown text',
},
],
},
},
};
const bindingState = {
entities: {
apps: {
main: {
bindings: [
{
app_id: 'appid',
location: AppBindingLocations.CHANNEL_HEADER_ICON,
icon: 'http://test.com/icon.png',
label: 'Label',
hint: 'Hint',
bindings: [
{
app_id: 'app1',
location: 'channel-header-1',
label: 'App 1 Channel Header',
form: {
submit: {
path: '/call/path',
},
},
},
],
},
],
},
},
general: {
config: {
FeatureFlagAppsEnabled: 'true',
},
},
},
};
test('renders the component correctly', () => {
const {container} = renderWithContext(
<WithTestMenuContext>
<MobileChannelHeaderPlugins
channel={channel}
isDropdown={true}
/>
</WithTestMenuContext>, {},
);
expect(container.firstChild).toBeNull();
});
test('renders the component correctly, with one extended component, and handle click event', () => {
renderWithContext(
<WithTestMenuContext>
<MobileChannelHeaderPlugins
channel={channel}
isDropdown={true}
/>
</WithTestMenuContext>, pluginState,
);
const menuItem = screen.getByText('some dropdown text');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem);
expect(action).toHaveBeenCalledTimes(1);
});
test('renders the component correctly, with two extended component', () => {
const testState = {
...pluginState,
plugins: {
...pluginState.plugins,
components: {
...pluginState.plugins.components,
MobileChannelHeaderButton: [
...pluginState.plugins.components.MobileChannelHeaderButton,
{
id: 'someid2',
pluginId: 'pluginid2',
icon: <i className='fa fa-anchor'/>,
action: jest.fn(),
dropdownText: 'some other dropdown text',
},
],
},
},
};
renderWithContext(
<WithTestMenuContext>
<MobileChannelHeaderPlugins
channel={channel}
isDropdown={true}
/>
</WithTestMenuContext>, testState,
);
const menuItem = screen.getByText('some dropdown text');
expect(menuItem).toBeInTheDocument();
const menuItem2 = screen.getByText('some other dropdown text');
expect(menuItem2).toBeInTheDocument();
});
test('renders the component correctly, with two extended bindings', () => {
const testState = cloneDeep(bindingState);
testState.entities.apps.main.bindings[0].bindings.push({
app_id: 'app2',
location: 'channel-header-2',
label: 'App 2 Channel Header',
form: {
submit: {
path: '/call/path',
},
},
});
renderWithContext(
<WithTestMenuContext>
<MobileChannelHeaderPlugins
channel={channel}
isDropdown={true}
/>
</WithTestMenuContext>, testState,
);
const menuItem = screen.getByText('App 1 Channel Header');
expect(menuItem).toBeInTheDocument();
const menuItem2 = screen.getByText('App 2 Channel Header');
expect(menuItem2).toBeInTheDocument();
});
test('Processes handleBinding, returns AppCallResponseTypes.OK', async () => {
jest.spyOn(appsActions, 'handleBindingClick').mockReturnValueOnce(() => {
return Promise.resolve({
data: {
type: AppCallResponseTypes.OK,
text: 'hello',
},
});
});
renderWithContext(
<WithTestMenuContext>
<MobileChannelHeaderPlugins
channel={channel}
isDropdown={true}
/>
</WithTestMenuContext>, bindingState,
);
const menuItem = screen.getByText('App 1 Channel Header');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem);
await waitFor(() => {
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(appsActions.handleBindingClick).toHaveBeenCalledTimes(1);
expect(appsActions.postEphemeralCallResponseForChannel).toHaveBeenCalledTimes(1);
});
});
test('Processes handleBinding, returns AppCallResponseTypes.Form', async () => {
jest.spyOn(appsActions, 'handleBindingClick').mockReturnValueOnce(() => {
return Promise.resolve({
data: {
type: AppCallResponseTypes.FORM,
form: {
submit: {
path: '/call/path',
},
},
},
});
});
renderWithContext(
<WithTestMenuContext>
<MobileChannelHeaderPlugins
channel={channel}
isDropdown={true}
/>
</WithTestMenuContext>, bindingState,
);
const menuItem = screen.getByText('App 1 Channel Header');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem);
await waitFor(() => {
// expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(appsActions.handleBindingClick).toHaveBeenCalledTimes(1);
expect(appsActions.openAppsModal).toHaveBeenCalledTimes(1);
});
});
test('Processes handleBinding, returns Error', async () => {
jest.spyOn(appsActions, 'handleBindingClick').mockReturnValueOnce(() => {
return Promise.resolve({
error: {
type: AppCallResponseTypes.ERROR,
text: 'Error returned from method',
},
});
});
renderWithContext(
<WithTestMenuContext>
<MobileChannelHeaderPlugins
channel={channel}
isDropdown={true}
/>
</WithTestMenuContext>, bindingState,
);
const menuItem = screen.getByText('App 1 Channel Header');
expect(menuItem).toBeInTheDocument();
fireEvent.click(menuItem);
await waitFor(() => {
// expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
expect(appsActions.handleBindingClick).toHaveBeenCalledTimes(1);
expect(appsActions.postEphemeralCallResponseForChannel).toHaveBeenCalledTimes(1);
});
});
test('renders the component correctly, with one extended component, isDropDown false', () => {
const action = jest.fn();
const pluginState = {
plugins: {
components: {
MobileChannelHeaderButton: [
{
id: 'someid',
pluginId: 'pluginid',
icon: <i className='fa fa-anchor'/>,
action,
dropdownText: 'some dropdown text',
},
],
},
},
};
renderWithContext(
<WithTestMenuContext>
<MobileChannelHeaderPlugins
channel={channel}
isDropdown={false}
/>
</WithTestMenuContext>, pluginState,
);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(action).toHaveBeenCalledTimes(1);
});
test('renders the component correctly, with one extended appbinding, isDropDown false', async () => {
jest.spyOn(appsActions, 'handleBindingClick').mockReturnValueOnce(() => {
return Promise.resolve({
data: {
type: AppCallResponseTypes.OK,
},
});
});
renderWithContext(
<WithTestMenuContext>
<MobileChannelHeaderPlugins
channel={channel}
isDropdown={false}
/>
</WithTestMenuContext>, bindingState,
);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
fireEvent.click(button);
await waitFor(() => {
expect(appsActions.handleBindingClick).toHaveBeenCalledTimes(1);
});
});
test('renders noting if multiple appbindings or components isDropDown false', () => {
const pluginState = {
plugins: {
components: {
MobileChannelHeaderButton: [
{
id: 'someid',
pluginId: 'pluginid',
icon: <i className='fa fa-anchor'/>,
action: jest.fn(),
dropdownText: 'some dropdown text',
},
],
},
},
};
const bothState = {
...bindingState,
...pluginState,
};
renderWithContext(
<WithTestMenuContext>
<MobileChannelHeaderPlugins
channel={channel}
isDropdown={false}
/>
</WithTestMenuContext>, bothState,
);
const button = screen.queryByRole('button');
expect(button).toBeNull();
});
});

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