From fd717cfa64a39a8a8d3d2cd1f312376cfa9bcd48 Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Wed, 19 Mar 2025 07:06:04 -0500 Subject: [PATCH] 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 --- .../account_settings/profile/nickname_spec.ts | 3 +- .../archive_channel_header_spec.ts | 46 +- .../archive_channel_member_spec.ts | 14 +- .../channels/channel/browse_channels_spec.ts | 2 +- .../channel/channel_header_modal_spec.js | 3 +- .../channel/channel_members_rhs_spec.ts | 7 +- .../channels/channel/channel_settings_spec.ts | 7 +- .../channel/close_direct_group_spec.ts | 4 +- .../convert_channel_to_private_spec.ts | 30 +- .../convert_group_message_to_private_spec.ts | 3 + ...ve_and_archive_channel_destructive_spec.ts | 24 +- .../add_users_to_channel_spec.ts | 83 +- .../channel_name_validations_spec.ts | 15 +- .../accessibility_modals_dialogs_1_spec.ts | 4 +- .../accessibility_modals_dialogs_spec.ts | 4 +- .../guest_identification_ui_spec.ts | 4 +- .../permissions/team_permissions_spec.ts | 27 +- .../about/edition_and_license_spec.js | 2 +- .../not_open_emoji_picker_spec.js | 12 +- .../archive_channel_mark_as_unread_spec.js | 2 +- .../channels/messaging/direct_message_spec.js | 8 +- .../channels/messaging/focus_move_spec.js | 3 +- .../channels/messaging/header_spec.js | 16 +- .../close_gm_via_menu_spec.js | 2 +- .../multi_team_and_dm/gm_add_user_spec.js | 4 +- .../multi_team_and_dm/gm_header_spec.js | 6 +- .../multi_team_and_dm/max_gm_members_spec.js | 5 +- .../users_deactivation_spec.js | 2 +- .../channels/general_actions_spec.js | 24 + .../playbooks/channels/run_spec.js | 1 + e2e-tests/cypress/tests/support/ui/channel.js | 16 +- .../tests/support/ui/channel_header.js | 1 - .../cypress/tests/support/ui_commands.ts | 36 +- .../channel_bookmarks_sub_menu/index.tsx | 75 - .../channel_header/channel_header_title.tsx | 113 +- .../channel_header_dropdown.test.tsx.snap | 1684 ----------------- .../channel_header_dropdown.test.tsx | 43 - .../channel_header_dropdown.tsx | 21 - .../channel_header_dropdown_items.tsx | 355 ---- .../channel_header_dropdown/index.ts | 102 - .../__snapshots__/close_channel.test.tsx.snap | 17 - .../close_channel/close_channel.test.tsx | 45 - .../close_channel/close_channel.tsx | 34 - .../menu_items/close_channel/index.ts | 18 - .../__snapshots__/close_message.test.tsx.snap | 17 - .../close_message/close_message.test.tsx | 74 - .../close_message/close_message.tsx | 113 -- .../menu_items/close_message/index.ts | 29 - .../__snapshots__/leave_channel.test.tsx.snap | 37 - .../menu_items/leave_channel/index.ts | 27 - .../leave_channel/leave_channel.test.tsx | 86 - .../leave_channel/leave_channel.tsx | 78 - .../menu_items/open_members_rhs/index.ts | 27 - .../open_members_rhs/open_members_rhs.tsx | 53 - .../toggle_favorite_channel.test.tsx.snap | 17 - .../toggle_favorite_channel/index.ts | 19 - .../toggle_favorite_channel.test.tsx | 88 - .../toggle_favorite_channel.tsx | 59 - .../menu_items/toggle_info/index.ts | 28 - .../menu_items/toggle_info/toggle_info.tsx | 50 - .../toggle_mute_channel.test.tsx.snap | 9 - .../menu_items/toggle_mute_channel/index.ts | 18 - .../toggle_mute_channel.test.tsx | 116 -- .../toggle_mute_channel.tsx | 79 - .../view_pinned_posts.test.tsx.snap | 9 - .../menu_items/view_pinned_posts/index.ts | 28 - .../view_pinned_posts.test.tsx | 51 - .../view_pinned_posts/view_pinned_posts.tsx | 49 - .../mobile_channel_header_dropdown.tsx | 90 - ...bile_channel_header_dropdown_animation.tsx | 36 - .../channel_header_menu.tsx | 181 ++ .../channel_header_direct_menu.tsx | 74 + .../channel_header_group_menu.tsx | 136 ++ .../channel_header_mobile_menu.tsx | 35 + .../channel_header_public_private_menu.tsx | 200 ++ .../menu_items/add_channel_members.test.tsx | 64 + .../menu_items/add_channel_members.tsx | 48 + .../menu_items/add_group_members.test.tsx | 60 + .../menu_items/add_group_members.tsx | 45 + .../menu_items/archive_channel.test.tsx | 104 + .../menu_items/archive_channel.tsx | 61 + .../menu_items/channel_bookmarks_submenu.tsx | 88 + .../menu_items/channel_settings_submenu.tsx | 167 ++ .../menu_items/close_channel.test.tsx | 36 + .../menu_items/close_channel.tsx | 26 + .../menu_items/close_message.test.tsx | 130 ++ .../menu_items/close_message.tsx | 81 + .../menu_items/convert_gm_to_private.test.tsx | 50 + .../menu_items/convert_gm_to_private.tsx | 47 + .../convert_public_to_private.test.tsx | 53 + .../menu_items/convert_public_to_private.tsx | 50 + .../menu_items/edit_channel_settings.test.tsx | 52 + .../menu_items/edit_channel_settings.tsx | 121 ++ .../edit_conversation_header.test.tsx | 50 + .../menu_items/edit_conversation_header.tsx | 49 + .../menu_items/groups.test.tsx | 70 + .../channel_header_menu/menu_items/groups.tsx | 74 + .../menu_items/leave_channel.test.tsx | 73 + .../menu_items/leave_channel.tsx | 63 + .../mobile_channel_header_plugins.test.tsx | 369 ++++ .../mobile_channel_header_plugins.tsx | 190 ++ .../menu_items/notification.test.tsx | 58 + .../menu_items/notification.tsx | 54 + .../menu_items/open_member_rhs.test.tsx | 97 + .../menu_items/open_members_rhs.tsx | 53 + .../menu_items/plugins_submenu.tsx | 43 + .../toggle_favorite_channel.test.tsx | 65 + .../menu_items/toggle_favorite_channel.tsx | 50 + .../menu_items/toggle_info.test.tsx | 79 + .../menu_items/toggle_info.tsx | 65 + .../menu_items/toggle_mute_channel.test.tsx | 126 ++ .../menu_items/toggle_mute_channel.tsx | 81 + .../menu_items/unarchive_channel.test.tsx | 49 + .../menu_items/unarchive_channel.tsx | 53 + .../menu_items/view_pinned_posts.test.tsx | 77 + .../menu_items/view_pinned_posts.tsx | 47 + .../channel_header_mobile.test.tsx.snap | 285 --- .../channel_header_mobile.test.tsx | 151 +- .../channel_header_mobile.tsx | 22 +- .../collapse_rhs_button.tsx | 7 + .../convert_gm_to_channel_modal.tsx | 6 +- .../convert_gm_to_channel_modal/index.ts | 12 +- .../sidebar_channel_menu.test.tsx.snap | 4 +- .../sidebar_channel_menu.tsx | 8 +- webapp/channels/src/i18n/en.json | 21 +- .../src/selectors/entities/channels.ts | 5 + .../mobile_channel_header_plug.test.tsx.snap | 1017 ---------- .../mobile_channel_header_plug/index.ts | 42 - .../mobile_channel_header_plug.test.tsx | 473 ----- .../mobile_channel_header_plug.tsx | 209 -- .../channels/src/sass/responsive/_mobile.scss | 130 -- webapp/channels/src/selectors/plugins.test.js | 32 +- webapp/channels/src/selectors/plugins.ts | 8 + 133 files changed, 4279 insertions(+), 6211 deletions(-) delete mode 100644 webapp/channels/src/components/channel_bookmarks_sub_menu/index.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap delete mode 100644 webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown.test.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown_items.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/index.ts delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/__snapshots__/close_channel.test.tsx.snap delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/close_channel.test.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/close_channel.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/index.ts delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/__snapshots__/close_message.test.tsx.snap delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/close_message.test.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/close_message.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/index.ts delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/__snapshots__/leave_channel.test.tsx.snap delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/index.ts delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.test.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/open_members_rhs/index.ts delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/open_members_rhs/open_members_rhs.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/__snapshots__/toggle_favorite_channel.test.tsx.snap delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/index.ts delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.test.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_info/index.ts delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_info/toggle_info.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/__snapshots__/toggle_mute_channel.test.tsx.snap delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/index.ts delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.test.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/__snapshots__/view_pinned_posts.test.tsx.snap delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/index.ts delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.test.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/mobile_channel_header_dropdown.tsx delete mode 100644 webapp/channels/src/components/channel_header_dropdown/mobile_channel_header_dropdown_animation.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/channel_header_menu.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_direct_menu.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_group_menu.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_mobile_menu.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_public_private_menu.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/add_channel_members.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/add_channel_members.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/add_group_members.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/add_group_members.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/channel_bookmarks_submenu.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/channel_settings_submenu.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/close_channel.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/close_channel.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/close_message.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/close_message.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/convert_gm_to_private.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/convert_gm_to_private.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/edit_conversation_header.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/edit_conversation_header.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/groups.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/groups.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/leave_channel.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/leave_channel.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/mobile_channel_header_plugins.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/mobile_channel_header_plugins.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/notification.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/notification.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/open_member_rhs.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/open_members_rhs.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/plugins_submenu.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/toggle_favorite_channel.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/toggle_favorite_channel.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/toggle_info.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/toggle_info.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/toggle_mute_channel.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/toggle_mute_channel.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/unarchive_channel.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/unarchive_channel.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/view_pinned_posts.test.tsx create mode 100644 webapp/channels/src/components/channel_header_menu/menu_items/view_pinned_posts.tsx delete mode 100644 webapp/channels/src/components/channel_header_mobile/__snapshots__/channel_header_mobile.test.tsx.snap delete mode 100644 webapp/channels/src/plugins/mobile_channel_header_plug/__snapshots__/mobile_channel_header_plug.test.tsx.snap delete mode 100644 webapp/channels/src/plugins/mobile_channel_header_plug/index.ts delete mode 100644 webapp/channels/src/plugins/mobile_channel_header_plug/mobile_channel_header_plug.test.tsx delete mode 100644 webapp/channels/src/plugins/mobile_channel_header_plug/mobile_channel_header_plug.tsx diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/nickname_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/nickname_spec.ts index f7e19a32f6..dac0017355 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/nickname_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/nickname_spec.ts @@ -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}`); diff --git a/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_header_spec.ts b/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_header_spec.ts index 998379a8a6..9ab70b38d9 100644 --- a/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_header_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_header_spec.ts @@ -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}'); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_member_spec.ts b/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_member_spec.ts index c91a3754c3..75b89503f3 100644 --- a/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_member_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_member_spec.ts @@ -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'); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.ts index 2396442511..ba85699355 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.ts @@ -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(); }); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/channel_header_modal_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/channel_header_modal_spec.js index e77b63beab..cac966dd60 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/channel_header_modal_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/channel_header_modal_spec.js @@ -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 diff --git a/e2e-tests/cypress/tests/integration/channels/channel/channel_members_rhs_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/channel_members_rhs_spec.ts index 5bece26d45..879464c35f 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/channel_members_rhs_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/channel_members_rhs_spec.ts @@ -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); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_spec.ts index bcfe332c9e..e39f4b3ba1 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/channel_settings_spec.ts @@ -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(); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/close_direct_group_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/close_direct_group_spec.ts index 1b74b9bf58..2174b3615d 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/close_direct_group_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/close_direct_group_spec.ts @@ -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); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/convert_channel_to_private_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/convert_channel_to_private_spec.ts index c4fad224db..032d784448 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/convert_channel_to_private_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/convert_channel_to_private_spec.ts @@ -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'); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/convert_group_message_to_private_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/convert_group_message_to_private_spec.ts index 08f7650a07..07674fdc72 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/convert_group_message_to_private_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/convert_group_message_to_private_spec.ts @@ -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); }); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts index 97f981bec2..5792f8d312 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/leave_and_archive_channel_destructive_spec.ts @@ -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 ', () => { diff --git a/e2e-tests/cypress/tests/integration/channels/channel_settings/add_users_to_channel_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_settings/add_users_to_channel_spec.ts index 5da4bf1899..cf80893770 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_settings/add_users_to_channel_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_settings/add_users_to_channel_spec.ts @@ -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 diff --git a/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts index 5160f77fe8..8e70ffe0dd 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts @@ -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(); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_1_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_1_spec.ts index 0397a8e2f4..b3733db527 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_1_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_1_spec.ts @@ -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); } diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.ts index 95fb568b85..e01479dca4 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.ts @@ -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(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts index 62712fd3fa..66fe20055b 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts @@ -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'); }); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts index 5d807a5675..866ba589f1 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts @@ -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'); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/edition_and_license_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/edition_and_license_spec.js index ac05be32d3..f5a86db202 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/edition_and_license_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/edition_and_license_spec.js @@ -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'); } } diff --git a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js index 39e722b0b1..16ae0b1cca 100644 --- a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js @@ -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(); diff --git a/e2e-tests/cypress/tests/integration/channels/mark_as_unread/archive_channel_mark_as_unread_spec.js b/e2e-tests/cypress/tests/integration/channels/mark_as_unread/archive_channel_mark_as_unread_spec.js index a7973363c7..a6385a035f 100644 --- a/e2e-tests/cypress/tests/integration/channels/mark_as_unread/archive_channel_mark_as_unread_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/mark_as_unread/archive_channel_mark_as_unread_spec.js @@ -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(); }); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/direct_message_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/direct_message_spec.js index 40c8ebb0ed..08ccaba91d 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/direct_message_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/direct_message_spec.js @@ -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'); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/focus_move_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/focus_move_spec.js index 1c032d8092..77d2e46666 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/focus_move_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/focus_move_spec.js @@ -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'); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/header_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/header_spec.js index d3269482d4..5a7dfbd303 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/header_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/header_spec.js @@ -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 === '>') { diff --git a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/close_gm_via_menu_spec.js b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/close_gm_via_menu_spec.js index 3eb259b664..b8365fd6ec 100644 --- a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/close_gm_via_menu_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/close_gm_via_menu_spec.js @@ -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(); diff --git a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_add_user_spec.js b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_add_user_spec.js index d2055a276d..3f278be614 100644 --- a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_add_user_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_add_user_spec.js @@ -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 diff --git a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_header_spec.js b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_header_spec.js index ae03812617..3bb9e50164 100644 --- a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_header_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_header_spec.js @@ -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); diff --git a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/max_gm_members_spec.js b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/max_gm_members_spec.js index 87c10f821d..c58b7b6e40 100644 --- a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/max_gm_members_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/max_gm_members_spec.js @@ -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); diff --git a/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_deactivation_spec.js b/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_deactivation_spec.js index 11a9bab3e2..03296cb69e 100644 --- a/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_deactivation_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_deactivation_spec.js @@ -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 diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/general_actions_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/general_actions_spec.js index b76b769966..e73d35e1ee 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/general_actions_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/general_actions_spec.js @@ -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 diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/run_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/run_spec.js index 0bda5ae0c2..4400dcafa3 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/run_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/run_spec.js @@ -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'); diff --git a/e2e-tests/cypress/tests/support/ui/channel.js b/e2e-tests/cypress/tests/support/ui/channel.js index 9b4de2238f..c4c18fdfec 100644 --- a/e2e-tests/cypress/tests/support/ui/channel.js +++ b/e2e-tests/cypress/tests/support/ui/channel.js @@ -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(); diff --git a/e2e-tests/cypress/tests/support/ui/channel_header.js b/e2e-tests/cypress/tests/support/ui/channel_header.js index eb3661257c..2429a72f83 100644 --- a/e2e-tests/cypress/tests/support/ui/channel_header.js +++ b/e2e-tests/cypress/tests/support/ui/channel_header.js @@ -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'); } diff --git a/e2e-tests/cypress/tests/support/ui_commands.ts b/e2e-tests/cypress/tests/support/ui_commands.ts index 4cf093365f..34ea4b0eb4 100644 --- a/e2e-tests/cypress/tests/support/ui_commands.ts +++ b/e2e-tests/cypress/tests/support/ui_commands.ts @@ -462,13 +462,15 @@ function getCurrentChannelId(): ChainableT { 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 { return cy.apiGetLDAPSync().then((response) => { const jobs = response.body; @@ -787,6 +807,12 @@ declare global { */ updateChannelHeader(text: string): ChainableT; + /** + * Update DM or GM channel header + * @param {String} text - Text to set the header to + */ + updateDMGMChannelHeader(text: string): ChainableT; + /** * Navigate to system console-PluginManagement from profile settings */ diff --git a/webapp/channels/src/components/channel_bookmarks_sub_menu/index.tsx b/webapp/channels/src/components/channel_bookmarks_sub_menu/index.tsx deleted file mode 100644 index 5687f2b553..0000000000 --- a/webapp/channels/src/components/channel_bookmarks_sub_menu/index.tsx +++ /dev/null @@ -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['subMenu'] = [ - { - id: 'channelBookmarksAddLink', - icon: , - direction: 'right', - text: formatMessage({id: 'channel_bookmarks.addLink', defaultMessage: 'Add a link'}), - action: handleCreateLink, - }, - ]; - - if (canUploadFiles) { - items.push({ - id: 'channelBookmarksAttachFile', - icon: , - direction: 'right', - text: formatMessage({id: 'channel_bookmarks.attachFile', defaultMessage: 'Attach a file'}), - action: handleCreateFile, - }); - } - - return ( - - ); -}; - -export default memo(ChannelBookmarksSubmenu); diff --git a/webapp/channels/src/components/channel_header/channel_header_title.tsx b/webapp/channels/src/components/channel_header/channel_header_title.tsx index 8373568523..058907158c 100644 --- a/webapp/channels/src/components/channel_header/channel_header_title.tsx +++ b/webapp/channels/src/components/channel_header/channel_header_title.tsx @@ -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(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 = ; } - let sharedIcon = null; + let sharedIcon; if (channel.shared) { sharedIcon = ( @@ -142,60 +99,12 @@ const ChannelHeaderTitle = ({ status={channel.status} /> )} - -
- -
- -
+ ); }; diff --git a/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap b/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap deleted file mode 100644 index 7eccb2fb7b..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/__snapshots__/channel_header_dropdown.test.tsx.snap +++ /dev/null @@ -1,1684 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/ChannelHeaderDropdown should match snapshot with no plugin items 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; diff --git a/webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown.test.tsx b/webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown.test.tsx deleted file mode 100644 index b551064fc0..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown.test.tsx +++ /dev/null @@ -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(); - 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(); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown.tsx b/webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown.tsx deleted file mode 100644 index 7312f13415..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown.tsx +++ /dev/null @@ -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; -}) => - ( - - - - ); - -export default memo(ChannelHeaderDropdown); diff --git a/webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown_items.tsx b/webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown_items.tsx deleted file mode 100644 index 8638a5dde8..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/channel_header_dropdown_items.tsx +++ /dev/null @@ -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 { - 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 = ( -
  • -
    -
  • - ); - } - - const pluginItems = this.props.pluginMenuItems.map((item) => { - return ( - { - if (item.action) { - item.action(channel.id); - } - }} - text={item.text} - /> - ); - }); - - return ( - <> - - {/* Remove when this components is migrated to new menus */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {isChannelBookmarksEnabled && } - - - - - - - - - - - - - {isMobile && - } - - - - - {pluginItems} - - - - - - - - ); - } -} diff --git a/webapp/channels/src/components/channel_header_dropdown/index.ts b/webapp/channels/src/components/channel_header_dropdown/index.ts deleted file mode 100644 index 662e323a0a..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/index.ts +++ /dev/null @@ -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); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/__snapshots__/close_channel.test.tsx.snap b/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/__snapshots__/close_channel.test.tsx.snap deleted file mode 100644 index 8645e597a9..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/__snapshots__/close_channel.test.tsx.snap +++ /dev/null @@ -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`] = ` - -`; - -exports[`components/ChannelHeaderDropdown/MenuItem.CloseChannel should match snapshot 1`] = ` - -`; diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/close_channel.test.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/close_channel.test.tsx deleted file mode 100644 index a6eec64adf..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/close_channel.test.tsx +++ /dev/null @@ -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(); - expect(wrapper).toMatchSnapshot(); - }); - - it('shoud be hidden if the channel is not archived', () => { - const props = { - ...baseProps, - isArchived: false, - }; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it('should runs goToLastViewedChannel function on click', () => { - const props = { - ...baseProps, - actions: { - ...baseProps.actions, - goToLastViewedChannel: jest.fn(), - }, - }; - const wrapper = shallow(); - wrapper.find(Menu.ItemAction).simulate('click'); - expect(props.actions.goToLastViewedChannel).toHaveBeenCalled(); - }); -}); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/close_channel.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/close_channel.tsx deleted file mode 100644 index 847b34b208..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/close_channel.tsx +++ /dev/null @@ -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 ( - - ); -}; - -export default React.memo(CloseChannel); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/index.ts b/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/index.ts deleted file mode 100644 index 7e81664075..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_channel/index.ts +++ /dev/null @@ -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); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/__snapshots__/close_message.test.tsx.snap b/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/__snapshots__/close_message.test.tsx.snap deleted file mode 100644 index bd5dd0e768..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/__snapshots__/close_message.test.tsx.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/ChannelHeaderDropdown/MenuItem.CloseMessage should match snapshot for DM Channel 1`] = ` - -`; - -exports[`components/ChannelHeaderDropdown/MenuItem.CloseMessage should match snapshot for GM Channel 1`] = ` - -`; diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/close_message.test.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/close_message.test.tsx deleted file mode 100644 index 0b88febb21..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/close_message.test.tsx +++ /dev/null @@ -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(); - expect(wrapper).toMatchSnapshot(); - }); - - it('should match snapshot for GM Channel', () => { - const props = {...baseProps, channel: groupChannel}; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it('should run savePreferences function on click for DM', () => { - const props = {...baseProps, channel: directChannel}; - const wrapper = shallow(); - 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(); - 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'}]); - }); -}); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/close_message.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/close_message.tsx deleted file mode 100644 index f4fa815c50..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/close_message.tsx +++ /dev/null @@ -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 { - 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 ( - - ); - } -} diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/index.ts b/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/index.ts deleted file mode 100644 index d5b89cb70d..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/close_message/index.ts +++ /dev/null @@ -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); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/__snapshots__/leave_channel.test.tsx.snap b/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/__snapshots__/leave_channel.test.tsx.snap deleted file mode 100644 index e6bf5d6dd0..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/__snapshots__/leave_channel.test.tsx.snap +++ /dev/null @@ -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`] = ` - -`; - -exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should be hidden if the channel type is DM or GM 1`] = ` - -`; - -exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should be hidden if the channel type is DM or GM 2`] = ` - -`; - -exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should match snapshot 1`] = ` - -`; diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/index.ts b/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/index.ts deleted file mode 100644 index f90ed0b17c..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/index.ts +++ /dev/null @@ -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; - -export default connector(LeaveChannel); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.test.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.test.tsx deleted file mode 100644 index 6df43b7057..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.test.tsx +++ /dev/null @@ -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(); - expect(wrapper).toMatchSnapshot(); - }); - - it('should be hidden if the channel is default channel', () => { - const props = { - ...baseProps, - isDefault: true, - }; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it('should be hidden if the channel type is DM or GM', () => { - const props = { - ...baseProps, - channel: {...baseProps.channel}, - }; - const makeWrapper = () => shallow(); - - 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(); - - 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, - }, - })); - }); -}); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.tsx deleted file mode 100644 index b2a89411be..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/leave_channel/leave_channel.tsx +++ /dev/null @@ -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 ( - - ); -}; - -export default memo(LeaveChannel); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/open_members_rhs/index.ts b/webapp/channels/src/components/channel_header_dropdown/menu_items/open_members_rhs/index.ts deleted file mode 100644 index d8a8633110..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/open_members_rhs/index.ts +++ /dev/null @@ -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); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/open_members_rhs/open_members_rhs.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/open_members_rhs/open_members_rhs.tsx deleted file mode 100644 index 8d15373a3c..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/open_members_rhs/open_members_rhs.tsx +++ /dev/null @@ -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 ( - - ); -}; - -export default ToggleChannelMembersRHS; diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/__snapshots__/toggle_favorite_channel.test.tsx.snap b/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/__snapshots__/toggle_favorite_channel.test.tsx.snap deleted file mode 100644 index fff0575feb..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/__snapshots__/toggle_favorite_channel.test.tsx.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel should match snapshot for favorite channel 1`] = ` - -`; - -exports[`components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel should match snapshot for not favorite channel 1`] = ` - -`; diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/index.ts b/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/index.ts deleted file mode 100644 index 385fe0aad3..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/index.ts +++ /dev/null @@ -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); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.test.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.test.tsx deleted file mode 100644 index 1abf295e32..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.test.tsx +++ /dev/null @@ -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(); - expect(wrapper).toMatchSnapshot(); - }); - - it('should runs unfavoriteChannel function for favorite channel', () => { - const wrapper = shallow(); - - 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(); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should runs favoriteChannel function for not favorite channel', () => { - const wrapper = shallow(); - - wrapper.find(Menu.ItemAction).simulate('click', { - preventDefault: jest.fn(), - }); - - expect(propsForNotFavorite.actions.favoriteChannel).toHaveBeenCalledWith(propsForFavorite.channel.id); - expect(propsForNotFavorite.actions.unfavoriteChannel).not.toHaveBeenCalled(); - }); -}); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.tsx deleted file mode 100644 index b9b9f41f9a..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_favorite_channel/toggle_favorite_channel.tsx +++ /dev/null @@ -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): 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 ( - - ); -}; - -export default memo(ToggleFavoriteChannel); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_info/index.ts b/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_info/index.ts deleted file mode 100644 index ef3f0c1223..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_info/index.ts +++ /dev/null @@ -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); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_info/toggle_info.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_info/toggle_info.tsx deleted file mode 100644 index d304618d68..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_info/toggle_info.tsx +++ /dev/null @@ -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 ( - - ); -}; - -export default ToggleInfo; diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/__snapshots__/toggle_mute_channel.test.tsx.snap b/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/__snapshots__/toggle_mute_channel.test.tsx.snap deleted file mode 100644 index e30a23d03f..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/__snapshots__/toggle_mute_channel.test.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/ChannelHeaderDropdown/MenuItemToggleMuteChannel should match snapshot 1`] = ` - -`; diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/index.ts b/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/index.ts deleted file mode 100644 index 3a1d1031b3..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/index.ts +++ /dev/null @@ -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); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.test.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.test.tsx deleted file mode 100644 index 08555fc2a9..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.test.tsx +++ /dev/null @@ -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(); - 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(); - - 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(); - - 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( - , - ); - 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( - , - ); - expect(wrapper.find(MenuItemAction).props().show).toEqual(true); - expect(wrapper.find(MenuItemAction).props().text).toEqual('Mute Conversation'); - }); - }); -}); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.tsx deleted file mode 100644 index eababb6912..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/toggle_mute_channel/toggle_mute_channel.tsx +++ /dev/null @@ -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): 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 ( - - ); -} diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/__snapshots__/view_pinned_posts.test.tsx.snap b/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/__snapshots__/view_pinned_posts.test.tsx.snap deleted file mode 100644 index a675341bc1..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/__snapshots__/view_pinned_posts.test.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/ChannelHeaderDropdown/MenuItem.ViewPinnedPosts should match snapshot 1`] = ` - -`; diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/index.ts b/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/index.ts deleted file mode 100644 index 92efbb16a9..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/index.ts +++ /dev/null @@ -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); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.test.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.test.tsx deleted file mode 100644 index 24ca52546b..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.test.tsx +++ /dev/null @@ -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(); - expect(wrapper).toMatchSnapshot(); - }); - - it('should runs closeRightHandSide function if has any pinned posts', () => { - const wrapper = shallow(); - - 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(); - - wrapper.find(Menu.ItemAction).simulate('click', { - preventDefault: jest.fn(), - }); - - expect(baseProps.actions.showPinnedPosts).toHaveBeenCalled(); - }); -}); diff --git a/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.tsx b/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.tsx deleted file mode 100644 index 89a57cdaac..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/menu_items/view_pinned_posts/view_pinned_posts.tsx +++ /dev/null @@ -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 ( - - ); -}; - -export default memo(ViewPinnedPosts); diff --git a/webapp/channels/src/components/channel_header_dropdown/mobile_channel_header_dropdown.tsx b/webapp/channels/src/components/channel_header_dropdown/mobile_channel_header_dropdown.tsx deleted file mode 100644 index 174b191f96..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/mobile_channel_header_dropdown.tsx +++ /dev/null @@ -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 ( - - ); - } - return displayName; - } - return channel.display_name; - }; - - let dmHeaderIconStatus; - - if (!teammateIsBot) { - dmHeaderIconStatus = ( - - ); - } - - return ( - - - - {dmHeaderIconStatus} - {getChannelTitle()} - - - - - - -
    - {'×'} -
    -
    -
    - ); -}; - -export default memo(MobileChannelHeaderDropdown); - diff --git a/webapp/channels/src/components/channel_header_dropdown/mobile_channel_header_dropdown_animation.tsx b/webapp/channels/src/components/channel_header_dropdown/mobile_channel_header_dropdown_animation.tsx deleted file mode 100644 index fa8bd74a2d..0000000000 --- a/webapp/channels/src/components/channel_header_dropdown/mobile_channel_header_dropdown_animation.tsx +++ /dev/null @@ -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 ( - - {children} - - ); -}; - -export default React.memo(MobileChannelHeaderDropdownAnimation); diff --git a/webapp/channels/src/components/channel_header_menu/channel_header_menu.tsx b/webapp/channels/src/components/channel_header_menu/channel_header_menu.tsx new file mode 100644 index 0000000000..4cf1900eb3 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/channel_header_menu.tsx @@ -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 = ; + 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 = ; + } + + const pluginItems = pluginMenuItems.map((item) => { + const handlePluginItemClick = () => { + if (item.action) { + item.action(channel.id); + } + }; + + return ( + {item.text}} + /> + ); + }); + + return ( + + {archivedIcon} + + {channelTitle as string} + + {sharedIcon} + + + ), + 'aria-label': ariaLabel.toLowerCase(), + }} + menu={{ + id: 'channelHeaderDropdownMenu', + }} + transformOrigin={{ + horizontal: 'left', + vertical: 'top', + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + > + {isDirect && ( + + )} + {isGroup && ( + + )} + {(!isDirect && !isGroup) && ( + + )} + + + + ); +} + diff --git a/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_direct_menu.tsx b/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_direct_menu.tsx new file mode 100644 index 0000000000..e8c3d273a2 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_direct_menu.tsx @@ -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 ( + <> + + + {isMobile && ( + <> + + + + )} + } + channel={channel} + /> + + + {!isMobile && ( + + )} + + + + ); +}; + +export default ChannelHeaderDirectMenu; diff --git a/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_group_menu.tsx b/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_group_menu.tsx new file mode 100644 index 0000000000..00b6340a40 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_group_menu.tsx @@ -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 ( + <> + + + {isMobile && ( + <> + + + + )} + {!isArchived && ( + + )} + {(isArchived && isGroupConstrained && isGuest(user.roles)) && ( + } + channel={channel} + /> + )} + {(!isArchived && !isGroupConstrained && !isGuest(user.roles)) && ( + + } + leadingElement={} + trailingElements={} + menuId={'channelSettings-menu'} + menuAriaLabel={formatMessage({id: 'channel_header.settings', defaultMessage: 'Settings'})} + > + + + + )} + + {(!isArchived && !isGroupConstrained) && ( + + + } + /> + + + )} + + {!isMobile && ( + + )} + + + + ); +}; + +export default ChannelHeaderGroupMenu; diff --git a/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_mobile_menu.tsx b/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_mobile_menu.tsx new file mode 100644 index 0000000000..1c0999201f --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_mobile_menu.tsx @@ -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 ( + <> + + + {props.pluginItems} + + ); +}; + +export default memo(ChannelHeaderMobileMenu); diff --git a/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_public_private_menu.tsx b/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_public_private_menu.tsx new file mode 100644 index 0000000000..23c0c66524 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/channel_header_menu_items/channel_header_public_private_menu.tsx @@ -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 ( + <> + + + {!isArchived && ( + <> + + + + + )} + + {isMobile && ( + <> + + + + + )} + + {(isArchived || isDefault) && ( + + } + /> + )} + + {!isArchived && !isDefault && ( + <> + + {isGroupConstrained && isLicensedForLDAPGroups && ( + + )} + + } + /> + + + + + } + /> + + + )} + + + + {!isMobile && ( + + )} + {!isDefault && ( + + )} + {!isDefault && !isGuest(user.roles) && ( + + )} + + {isArchived && ( + + )} + + {!isArchived && !isDefault && ( + + + + )} + + {isArchived && !isDefault && ( + + + + )} + + ); +}; + +export default ChannelHeaderPublicMenu; diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/add_channel_members.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/add_channel_members.test.tsx new file mode 100644 index 0000000000..4e5c358b91 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/add_channel_members.test.tsx @@ -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( + , {}, + ); + + const menuItem = screen.getByText('Add Members'); + expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders + }); + + test('dispatches openModal action on click', () => { + renderWithContext( + + + , {}, + ); + + 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}, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/add_channel_members.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/add_channel_members.tsx new file mode 100644 index 0000000000..d15a6beecd --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/add_channel_members.tsx @@ -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 ( + } + onClick={handleAddMembers} + labels={ + + } + /> + ); +}; + +export default React.memo(AddChannelMembers); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/add_group_members.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/add_group_members.test.tsx new file mode 100644 index 0000000000..cc28c3fdb1 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/add_group_members.test.tsx @@ -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( + , {}, + ); + + const menuItem = screen.getByText('Add Members'); + expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders + }); + + test('dispatches openModal action on click', () => { + renderWithContext( + + + , {}, + ); + + 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, + }, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/add_group_members.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/add_group_members.tsx new file mode 100644 index 0000000000..99a5e90e30 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/add_group_members.tsx @@ -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 ( + } + onClick={handleAddGroupMembers} + labels={ + + } + /> + ); +}; + +export default React.memo(AddGroupMembers); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.test.tsx new file mode 100644 index 0000000000..dd583b02da --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.test.tsx @@ -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( + , 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( + + + , 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', + }, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.tsx new file mode 100644 index 0000000000..f929d64b1b --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.tsx @@ -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 ( + } + onClick={handleArchiveChannel} + labels={ + + } + isDestructive={true} + /> + ); +}; + +export default memo(ArchiveChannel); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/channel_bookmarks_submenu.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/channel_bookmarks_submenu.tsx new file mode 100644 index 0000000000..2cd589ab3f --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/channel_bookmarks_submenu.tsx @@ -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 ( + } + labels={( + + )} + trailingElements={( + + )} + menuId={`channel-menu-${props.channel.id}-menu`} + menuAriaLabel={formatMessage({id: 'channel_menu.bookmarks', defaultMessage: 'Bookmarks Bar'})} + > + } + labels={( + + )} + onClick={() => handleCreateLink()} + /> + {canUploadFiles && ( + } + labels={( + + )} + onClick={() => handleCreateFile()} + /> + )} + + ); +}; + +export default memo(ChannelBookmarksSubmenu); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/channel_settings_submenu.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/channel_settings_submenu.tsx new file mode 100644 index 0000000000..5ce9472acf --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/channel_settings_submenu.tsx @@ -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 ( + + } + leadingElement={} + trailingElements={} + menuId={'channelSettings-menu'} + menuAriaLabel={formatMessage({id: 'channelSettings', defaultMessage: 'Channel Settings'})} + > + {!isReadonly && ( + + } + /> + )} + + {!isReadonly && ( + + + } + /> + + )} + + {!isReadonly && ( + + + } + /> + + )} + + {!isDefault && channel.type === Constants.OPEN_CHANNEL && ( + + + } + /> + + )} + + ); +}; + +export default memo(ChannelSettingsSubmenu); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/close_channel.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/close_channel.test.tsx new file mode 100644 index 0000000000..f89ecfaa74 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/close_channel.test.tsx @@ -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( + + + , {}, + ); + + 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 + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/close_channel.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/close_channel.tsx new file mode 100644 index 0000000000..558de52fee --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/close_channel.tsx @@ -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 ( + } + {...rest} + /> + ); +}; +export default React.memo(CloseChannel); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/close_message.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/close_message.test.tsx new file mode 100644 index 0000000000..1ab8cd5b4b --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/close_message.test.tsx @@ -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( + + + , 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( + + + , 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'}], + ); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/close_message.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/close_message.tsx new file mode 100644 index 0000000000..728173642f --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/close_message.tsx @@ -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 = ( + ); + if (channel.type === Constants.GM_CHANNEL) { + text = ( + ); + } + + return ( + } + onClick={handleClose} + labels={text} + /> + ); +} diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/convert_gm_to_private.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/convert_gm_to_private.test.tsx new file mode 100644 index 0000000000..45e042698b --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/convert_gm_to_private.test.tsx @@ -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( + + + , {}, + ); + + 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}, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/convert_gm_to_private.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/convert_gm_to_private.tsx new file mode 100644 index 0000000000..7ce00f2acf --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/convert_gm_to_private.tsx @@ -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 ( + + } + /> + ); +}; + +export default React.memo(ConvertGMtoPrivate); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.test.tsx new file mode 100644 index 0000000000..87f0a572ee --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.test.tsx @@ -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( + + + , {}, + ); + + 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, + }, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.tsx new file mode 100644 index 0000000000..0477a303f0 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/convert_public_to_private.tsx @@ -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 ( + + } + /> + ); +}; + +export default React.memo(ConvertPublictoPrivate); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.test.tsx new file mode 100644 index 0000000000..fd0e38c8bd --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.test.tsx @@ -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( + + + , {}, + ); + + 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}, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.tsx new file mode 100644 index 0000000000..cd70ccd9b6 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/edit_channel_settings.tsx @@ -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 && ( + <> + + } + /> + + + } + /> + + } + /> + + + )} + + {!isDefault && channel.type === Constants.OPEN_CHANNEL && ( + + + + )} + + ); +}; + +export default React.memo(EditChannelSettings); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/edit_conversation_header.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/edit_conversation_header.test.tsx new file mode 100644 index 0000000000..b4c5d64eba --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/edit_conversation_header.test.tsx @@ -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( + + + , {}, + ); + + 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}, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/edit_conversation_header.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/edit_conversation_header.tsx new file mode 100644 index 0000000000..38df20e73f --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/edit_conversation_header.tsx @@ -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 ( + + } + /> + ); +}; + +export default React.memo(EditConversationHeader); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/groups.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/groups.test.tsx new file mode 100644 index 0000000000..a0699880f0 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/groups.test.tsx @@ -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( + + + , {}, + ); + + 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( + + + , {}, + ); + + 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}, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/groups.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/groups.tsx new file mode 100644 index 0000000000..a0ca290b56 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/groups.tsx @@ -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 ( + <> + } + onClick={handleAddGroups} + labels={ + + } + {...rest} + /> + } + onClick={handleManageGroups} + labels={ + + } + {...rest} + /> + + ); +}; + +export default React.memo(Groups); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/leave_channel.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/leave_channel.test.tsx new file mode 100644 index 0000000000..b3f031ed82 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/leave_channel.test.tsx @@ -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( + + + , {}, + ); + + 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( + + + , {}, + ); + + 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, + }, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/leave_channel.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/leave_channel.tsx new file mode 100644 index 0000000000..7dc78b001b --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/leave_channel.tsx @@ -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 ( + } + onClick={handleLeave} + labels={ + + } + isDestructive={true} + /> + ); +}; + +export default memo(LeaveChannel); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/mobile_channel_header_plugins.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/mobile_channel_header_plugins.test.tsx new file mode 100644 index 0000000000..a95781414b --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/mobile_channel_header_plugins.test.tsx @@ -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: , + 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( + + + , {}, + ); + expect(container.firstChild).toBeNull(); + }); + + test('renders the component correctly, with one extended component, and handle click event', () => { + renderWithContext( + + + , 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: , + action: jest.fn(), + dropdownText: 'some other dropdown text', + }, + ], + }, + }, + }; + + renderWithContext( + + + , 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( + + + , 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( + + + , 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( + + + , 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( + + + , 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: , + action, + dropdownText: 'some dropdown text', + }, + ], + }, + }, + }; + + renderWithContext( + + + , 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( + + + , 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: , + action: jest.fn(), + dropdownText: 'some dropdown text', + }, + ], + }, + }, + }; + const bothState = { + ...bindingState, + ...pluginState, + }; + + renderWithContext( + + + , bothState, + ); + const button = screen.queryByRole('button'); + expect(button).toBeNull(); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/mobile_channel_header_plugins.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/mobile_channel_header_plugins.tsx new file mode 100644 index 0000000000..3ebc809ad0 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/mobile_channel_header_plugins.tsx @@ -0,0 +1,190 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {memo} from 'react'; +import {useIntl} from 'react-intl'; +import {useSelector, useDispatch} from 'react-redux'; + +import type {AppBinding} from '@mattermost/types/apps'; +import type {Channel} from '@mattermost/types/channels'; + +import {AppCallResponseTypes, AppBindingLocations} from 'mattermost-redux/constants/apps'; +import {makeAppBindingsSelector} from 'mattermost-redux/selectors/entities/apps'; +import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; + +import {handleBindingClick, openAppsModal, postEphemeralCallResponseForChannel} from 'actions/apps'; +import {getChannelMobileHeaderPluginButtons} from 'selectors/plugins'; + +import * as Menu from 'components/menu'; + +import {createCallContext} from 'utils/apps'; + +import type {MobileChannelHeaderButtonAction} from 'types/store/plugins'; + +type Props = { + channel: Channel; + isDropdown: boolean; +} + +const MobileChannelHeaderPlugins = (props: Props): JSX.Element => { + const mobileComponents = useSelector(getChannelMobileHeaderPluginButtons); + const channelMember = useSelector(getMyCurrentChannelMembership); + const getChannelHeaderBindings = useSelector(makeAppBindingsSelector(AppBindingLocations.CHANNEL_HEADER_ICON)); + const intl = useIntl(); + const dispatch = useDispatch(); + + const createAppButton = (binding: AppBinding) => { + const handleAppButtonClick = () => fireAppAction(binding); + + if (props.isDropdown) { + return ( + {binding.label}} + /> + ); + } + return ( +
  • + +
  • + ); + }; + + const createButton = (plug: MobileChannelHeaderButtonAction) => { + const handlePluginButtonClick = () => fireAction(plug); + + if (props.isDropdown) { + return ( + {plug.dropdownText}} + /> + ); + } + + return ( +
  • + +
  • + ); + }; + + const createList = (plugs: MobileChannelHeaderButtonAction[]) => { + return plugs.map(createButton); + }; + + const createAppList = (bindings: AppBinding[]) => { + return bindings.map(createAppButton); + }; + + const fireAction = (plug: MobileChannelHeaderButtonAction) => { + return plug.action?.(props.channel, channelMember); + }; + + const fireAppAction = async (binding: AppBinding) => { + const {channel} = props; + const context = createCallContext( + binding.app_id, + binding.location, + channel.id, + channel.team_id, + ); + + const handleAppResponse = (callResp: any, errorResponse?: any) => { + if (errorResponse) { + const errorMessage = errorResponse.text || intl.formatMessage({ + id: 'apps.error.unknown', + defaultMessage: 'Unknown error occurred.', + }); + dispatch(postEphemeralCallResponseForChannel(errorResponse, errorMessage, channel.id)); + return; + } + + switch (callResp.type) { + case AppCallResponseTypes.OK: + if (callResp.text) { + dispatch(postEphemeralCallResponseForChannel(callResp, callResp.text, channel.id)); + } + break; + case AppCallResponseTypes.NAVIGATE: + break; + case AppCallResponseTypes.FORM: + if (callResp.form) { + dispatch(openAppsModal(callResp.form, context)); + } + break; + default: { + const errorMessage = intl.formatMessage({ + id: 'apps.error.responses.unknown_type', + defaultMessage: 'App response type not supported. Response type: {type}.', + }, { + type: callResp.type, + }); + dispatch(postEphemeralCallResponseForChannel(callResp, errorMessage, channel.id)); + } + } + }; + + const res = await dispatch(handleBindingClick(binding, context, intl)); + if (res.error) { + handleAppResponse(null, res.error); + return; + } + handleAppResponse(res.data!); + }; + + const components = mobileComponents || []; + const bindings = getChannelHeaderBindings || []; + + if (components.length === 0 && bindings.length === 0) { + return <>; + } else if (components.length === 1 && bindings.length === 0) { + return createButton(components[0]); + } else if (components.length === 0 && bindings.length === 1) { + return createAppButton(bindings[0]); + } + + if (!props.isDropdown) { + return <>; + } + + const plugItems = createList(components); + const appItems = createAppList(bindings); + return ( + <> + + {appItems} + {plugItems} + + + ); +}; + +// Exported for tests +export {MobileChannelHeaderPlugins as RawMobileChannelHeaderPlug}; + +export default memo(MobileChannelHeaderPlugins); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/notification.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/notification.test.tsx new file mode 100644 index 0000000000..3b2f20d7c2 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/notification.test.tsx @@ -0,0 +1,58 @@ +// 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 ChannelNotificationsModal from 'components/channel_notifications_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 Notification from './notification'; + +describe('components/ChannelHeaderMenu/MenuItems/Notification', () => { + 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, handle click event', () => { + const channel = TestHelper.getChannelMock(); + const user = TestHelper.getUserMock(); + + renderWithContext( + + + , {}, + ); + + const menuItemMG = screen.getByText('Notification Preferences'); + 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.CHANNEL_NOTIFICATIONS, + dialogType: ChannelNotificationsModal, + dialogProps: { + channel, + currentUser: user, + }, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/notification.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/notification.tsx new file mode 100644 index 0000000000..96c17d8abb --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/notification.tsx @@ -0,0 +1,54 @@ +// 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 {BellOutlineIcon} from '@mattermost/compass-icons/components'; +import type {Channel} from '@mattermost/types/channels'; +import type {UserProfile} from '@mattermost/types/users'; + +import {openModal} from 'actions/views/modals'; + +import ChannelNotificationsModal from 'components/channel_notifications_modal'; +import * as Menu from 'components/menu'; + +import {ModalIdentifiers} from 'utils/constants'; + +interface Props extends Menu.FirstMenuItemProps { + channel: Channel; + user: UserProfile; +} + +const Notification = ({channel, user, ...rest}: Props): JSX.Element => { + const dispatch = useDispatch(); + + const handleNotificationPreferences = () => { + dispatch(openModal({ + modalId: ModalIdentifiers.CHANNEL_NOTIFICATIONS, + dialogType: ChannelNotificationsModal, + dialogProps: { + channel, + currentUser: user, + }, + })); + }; + + return ( + } + id='channelNotificationPreferences' + onClick={handleNotificationPreferences} + labels={ + + } + {...rest} + /> + ); +}; + +export default React.memo(Notification); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/open_member_rhs.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/open_member_rhs.test.tsx new file mode 100644 index 0000000000..433d84fef7 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/open_member_rhs.test.tsx @@ -0,0 +1,97 @@ +// 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 * as rhsActions from 'actions/views/rhs'; + +import {WithTestMenuContext} from 'components/menu/menu_context_test'; + +import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils'; +import {RHSStates} from 'utils/constants'; +import {TestHelper} from 'utils/test_helper'; + +import OpenMembersRHS from './open_members_rhs'; + +describe('components/ChannelHeaderMenu/MenuItems/OpenMembersRHS', () => { + beforeEach(() => { + // jest.spyOn(rhsActions, 'closeRightHandSide').mockImplementation(() => () => ({data: true})); + jest.spyOn(rhsActions, 'showChannelMembers').mockReturnValue(() => Promise.resolve({data: true})); + + jest.spyOn(require('react-redux'), 'useDispatch'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders the component correctly, handles click event, rhs closed', () => { + const state = { + views: { + rhs: { + rhsState: '', + }, + }, + }; + const channel = TestHelper.getChannelMock(); + + renderWithContext( + + + } + /> + , state, + ); + + const menuItem = screen.getByText('View Members'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(rhsActions.showChannelMembers).toHaveBeenCalledTimes(1); + expect(rhsActions.showChannelMembers).toHaveBeenCalledWith(channel.id, false); + }); + + test('renders the component correctly, handles correct click event, rhs open', () => { + const state = { + views: { + rhs: { + rhsState: RHSStates.CHANNEL_MEMBERS, + isSidebarOpen: true, + }, + }, + }; + + const channel = TestHelper.getChannelMock(); + + renderWithContext( + + + } + /> + , state, + ); + + const menuItem = screen.getByText('View Members'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu + expect(rhsActions.showChannelMembers).not.toHaveBeenCalled(); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/open_members_rhs.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/open_members_rhs.tsx new file mode 100644 index 0000000000..0ad5bef0f3 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/open_members_rhs.tsx @@ -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, useSelector} from 'react-redux'; + +import {AccountOutlineIcon} from '@mattermost/compass-icons/components'; +import type {Channel} from '@mattermost/types/channels'; + +import {showChannelMembers} from 'actions/views/rhs'; +import {getIsRhsOpen, getRhsState} from 'selectors/rhs'; + +import * as Menu from 'components/menu'; + +import {RHSStates} from 'utils/constants'; + +type Props = { + channel: Channel; + id: string; + editMembers?: boolean; + text: React.ReactElement; +}; + +const OpenMembersRHS = ({ + id, + channel, + text, + editMembers = false, +}: Props) => { + const dispatch = useDispatch(); + let rhsOpen = useSelector(getIsRhsOpen); + const rhsState = useSelector(getRhsState); + if (rhsState !== RHSStates.CHANNEL_MEMBERS) { + rhsOpen = false; + } + const openRHSIfNotOpen = () => { + if (rhsOpen) { + return; + } + dispatch(showChannelMembers(channel.id, editMembers)); + }; + + return ( + } + id={id} + onClick={openRHSIfNotOpen} + labels={text} + /> + ); +}; + +export default OpenMembersRHS; diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/plugins_submenu.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/plugins_submenu.tsx new file mode 100644 index 0000000000..419511e944 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/plugins_submenu.tsx @@ -0,0 +1,43 @@ +// 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 {FormattedMessage, useIntl} from 'react-intl'; + +import { + AppsIcon, + ChevronRightIcon, +} from '@mattermost/compass-icons/components'; + +import * as Menu from 'components/menu'; + +type Props = { + pluginItems: ReactNode[]; +}; + +const PluginsSubmenu = (props: Props) => { + const {formatMessage} = useIntl(); + if (!props.pluginItems || !props.pluginItems.length) { + return <>; + } + return ( + + } + leadingElement={} + trailingElements={} + menuId={'moreActions-menu'} + menuAriaLabel={formatMessage({id: 'pluginsMenu.more_actions', defaultMessage: 'More actions'})} + > + {props.pluginItems} + + ); +}; + +export default memo(PluginsSubmenu); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/toggle_favorite_channel.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_favorite_channel.test.tsx new file mode 100644 index 0000000000..8d2e4b9bdc --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_favorite_channel.test.tsx @@ -0,0 +1,65 @@ +// 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 'mattermost-redux/actions/channels'; + +import {WithTestMenuContext} from 'components/menu/menu_context_test'; + +import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils'; +import {TestHelper} from 'utils/test_helper'; + +import ToggleFavoriteChannel from './toggle_favorite_channel'; + +describe('components/ChannelHeaderMenu/MenuItems/ToggleFavoriteChannel', () => { + const channel = TestHelper.getChannelMock(); + + beforeEach(() => { + jest.spyOn(channelActions, 'favoriteChannel').mockReturnValue(() => Promise.resolve({data: true})); + jest.spyOn(channelActions, 'unfavoriteChannel'); + jest.spyOn(require('react-redux'), 'useDispatch'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders the component correctly, handles correct click event, is favorite false', () => { + renderWithContext( + + + , {}, + ); + + const menuItem = screen.getByText('Add to Favorites'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + // expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(channelActions.favoriteChannel).toHaveBeenCalledTimes(1); + expect(channelActions.favoriteChannel).toHaveBeenCalledWith(channel.id); + }); + + test('renders the component correctly, handles correct click event, is favorite true', () => { + renderWithContext( + + + , {}, + ); + + const menuItem = screen.getByText('Remove from Favorites'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(channelActions.unfavoriteChannel).toHaveBeenCalledTimes(1); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/toggle_favorite_channel.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_favorite_channel.tsx new file mode 100644 index 0000000000..d9716b842d --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_favorite_channel.tsx @@ -0,0 +1,50 @@ +// 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 {favoriteChannel, unfavoriteChannel} from 'mattermost-redux/actions/channels'; + +import * as Menu from 'components/menu'; + +type Props = { + channelID: string; + isFavorite: boolean; +}; + +const ToggleFavoriteChannel = ({ + isFavorite, + channelID, +}: Props) => { + const dispatch = useDispatch(); + const toggleFavorite = () => { + if (isFavorite) { + dispatch(unfavoriteChannel(channelID)); + } else { + dispatch(favoriteChannel(channelID)); + } + }; + + let text = ( + ); + if (isFavorite) { + text = ( + ); + } + return ( + + ); +}; + +export default memo(ToggleFavoriteChannel); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/toggle_info.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_info.test.tsx new file mode 100644 index 0000000000..cf09faa08f --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_info.test.tsx @@ -0,0 +1,79 @@ +// 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 rhsActions from 'actions/views/rhs'; + +import {WithTestMenuContext} from 'components/menu/menu_context_test'; + +import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils'; +import {RHSStates} from 'utils/constants'; +import {TestHelper} from 'utils/test_helper'; + +import ToggleInfo from './toggle_info'; + +describe('components/ChannelHeaderMenu/MenuItems/ToggleInfo', () => { + beforeEach(() => { + jest.spyOn(rhsActions, 'closeRightHandSide').mockImplementation(() => () => ({data: true})); + jest.spyOn(rhsActions, 'showChannelInfo'); + + jest.spyOn(require('react-redux'), 'useDispatch'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders the component correctly, handles click event, rhs closed', () => { + const state = { + views: { + rhs: { + rhsState: '', + }, + }, + }; + const channel = TestHelper.getChannelMock(); + + renderWithContext( + + + , state, + ); + + const menuItem = screen.getByText('View Info'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + // expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(rhsActions.showChannelInfo).toHaveBeenCalledTimes(1); + expect(rhsActions.showChannelInfo).toHaveBeenCalledWith(channel.id); + }); + + test('renders the component correctly, handles correct click event, rhs open', () => { + const state = { + views: { + rhs: { + rhsState: RHSStates.CHANNEL_INFO, + isSidebarOpen: true, + }, + }, + }; + + const channel = TestHelper.getChannelMock(); + + renderWithContext( + + + , state, + ); + + const menuItem = screen.getByText('Close Info'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(rhsActions.closeRightHandSide).toHaveBeenCalledTimes(1); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/toggle_info.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_info.tsx new file mode 100644 index 0000000000..01b6a07161 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_info.tsx @@ -0,0 +1,65 @@ +// 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 {useSelector, useDispatch} from 'react-redux'; + +import {InformationOutlineIcon} from '@mattermost/compass-icons/components'; +import type {Channel} from '@mattermost/types/channels'; + +import {closeRightHandSide, showChannelInfo} from 'actions/views/rhs'; +import {getIsRhsOpen, getRhsState} from 'selectors/rhs'; + +import * as Menu from 'components/menu'; + +import {RHSStates} from 'utils/constants'; + +interface Props extends Menu.FirstMenuItemProps { + channel: Channel; +} + +const ToggleInfo = ({channel, ...rest}: Props) => { + const dispatch = useDispatch(); + let rhsOpen = useSelector(getIsRhsOpen); + const rhsState = useSelector(getRhsState); + if (rhsState !== RHSStates.CHANNEL_INFO) { + rhsOpen = false; + } + + const toggleRHS = () => { + if (rhsOpen) { + dispatch(closeRightHandSide()); + return; + } + dispatch(showChannelInfo(channel.id)); + }; + + let text; + if (rhsOpen) { + text = ( + ); + } else { + text = ( + ); + } + + return ( + <> + } + onClick={toggleRHS} + labels={text} + {...rest} + /> + + ); +}; + +export default ToggleInfo; diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/toggle_mute_channel.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_mute_channel.test.tsx new file mode 100644 index 0000000000..c37c33a916 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_mute_channel.test.tsx @@ -0,0 +1,126 @@ +// 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 'mattermost-redux/actions/channels'; + +import {WithTestMenuContext} from 'components/menu/menu_context_test'; + +import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils'; +import {NotificationLevels} from 'utils/constants'; +import {TestHelper} from 'utils/test_helper'; + +import ToggleMuteChannel from './toggle_mute_channel'; + +describe('components/ChannelHeaderMenu/MenuItems/ToggleMuteChannel', () => { + const channel = TestHelper.getChannelMock(); + const user = TestHelper.getUserMock(); + + beforeEach(() => { + jest.spyOn(channelActions, 'updateChannelNotifyProps').mockReturnValue(() => Promise.resolve({data: true})); + jest.spyOn(require('react-redux'), 'useDispatch'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders the component correctly, public channel, not muted', () => { + renderWithContext( + + + , {}, + ); + + const menuItem = screen.getByText('Mute Channel'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledTimes(1); + expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledWith( + user.id, + channel.id, + {mark_unread: NotificationLevels.MENTION}, + ); + }); + + test('renders the component correctly, public channel, muted', () => { + renderWithContext( + + + , {}, + ); + + const menuItem = screen.getByText('Unmute Channel'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledTimes(1); + expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledWith( + user.id, + channel.id, + {mark_unread: NotificationLevels.ALL}, + ); + }); + + test('renders the component correctly, dm channel, not muted', () => { + const channel = TestHelper.getChannelMock({type: 'D'}); + renderWithContext( + + + , {}, + ); + + const menuItem = screen.getByText('Mute'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledTimes(1); + expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledWith( + user.id, + channel.id, + {mark_unread: NotificationLevels.MENTION}, + ); + }); + test('renders the component correctly, dm channel, muted', () => { + const channel = TestHelper.getChannelMock({type: 'D'}); + renderWithContext( + + + , {}, + ); + + const menuItem = screen.getByText('Unmute'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledTimes(1); + expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledWith( + user.id, + channel.id, + {mark_unread: NotificationLevels.ALL}, + ); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/toggle_mute_channel.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_mute_channel.tsx new file mode 100644 index 0000000000..204343f1ea --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/toggle_mute_channel.tsx @@ -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} from 'react-redux'; + +import {BellOffOutlineIcon} from '@mattermost/compass-icons/components'; +import type {Channel} from '@mattermost/types/channels'; + +import {updateChannelNotifyProps} from 'mattermost-redux/actions/channels'; + +import * as Menu from 'components/menu'; + +import {Constants, NotificationLevels} from 'utils/constants'; + +type Props = { + userID: string; + channel: Channel; + isMuted: boolean; +}; + +export default function ToggleMuteChannel({ + isMuted, + channel, + userID, +}: Props) { + const dispatch = useDispatch(); + + const handleClick = () => { + dispatch(updateChannelNotifyProps( + userID, + channel.id, + { + mark_unread: (isMuted ? NotificationLevels.ALL : NotificationLevels.MENTION) as 'all' | 'mention', + }, + )); + }; + + let text; + if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) { + if (isMuted) { + text = ( + + ); + } else { + text = ( + + ); + } + } else if (isMuted) { + text = ( + + ); + } else { + text = ( + + ); + } + + return ( + } + id='channelToggleMuteChannel' + onClick={handleClick} + labels={text} + /> + ); +} diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/unarchive_channel.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/unarchive_channel.test.tsx new file mode 100644 index 0000000000..172405d03a --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/unarchive_channel.test.tsx @@ -0,0 +1,49 @@ +// 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 UnarchiveChannelModal from 'components/unarchive_channel_modal'; + +import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils'; +import {ModalIdentifiers} from 'utils/constants'; +import {TestHelper} from 'utils/test_helper'; + +import UnarchiveChannel from './unarchive_channel'; + +describe('components/ChannelHeaderMenu/MenuItems/UnarchiveChannel', () => { + beforeEach(() => { + jest.spyOn(modalActions, 'openModal'); + jest.spyOn(require('react-redux'), 'useDispatch'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders the component correctly, handle click event', () => { + const channel = TestHelper.getChannelMock(); + + renderWithContext( + + + , {}, + ); + + const menuItem = screen.getByText('Unarchive 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.UNARCHIVE_CHANNEL, + dialogType: UnarchiveChannelModal, + dialogProps: {channel}, + }); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/unarchive_channel.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/unarchive_channel.tsx new file mode 100644 index 0000000000..96fb2dad05 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/unarchive_channel.tsx @@ -0,0 +1,53 @@ +// 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 type {Channel} from '@mattermost/types/channels'; + +import {openModal} from 'actions/views/modals'; + +import * as Menu from 'components/menu'; +import UnarchiveChannelModal from 'components/unarchive_channel_modal'; + +import {ModalIdentifiers} from 'utils/constants'; + +type Props = { + channel: Channel; +} + +const UnarchiveChannel = ({ + channel, +}: Props) => { + const dispatch = useDispatch(); + + const handleUnarchiveChannel = () => { + dispatch( + openModal({ + modalId: ModalIdentifiers.UNARCHIVE_CHANNEL, + dialogType: UnarchiveChannelModal, + dialogProps: {channel}, + }), + ); + }; + + return ( + <> + + + } + /> + + ); +}; + +export default memo(UnarchiveChannel); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/view_pinned_posts.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/view_pinned_posts.test.tsx new file mode 100644 index 0000000000..2e205ba939 --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/view_pinned_posts.test.tsx @@ -0,0 +1,77 @@ +// 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 rhsActions from 'actions/views/rhs'; + +import {WithTestMenuContext} from 'components/menu/menu_context_test'; + +import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils'; +import {RHSStates} from 'utils/constants'; +import {TestHelper} from 'utils/test_helper'; + +import ViewPinnedPosts from './view_pinned_posts'; + +describe('components/ChannelHeaderMenu/MenuItems/ViewPinnedPosts', () => { + beforeEach(() => { + jest.spyOn(rhsActions, 'closeRightHandSide').mockImplementation(() => () => ({data: true})); + jest.spyOn(rhsActions, 'showPinnedPosts').mockReturnValue(() => Promise.resolve({data: true})); + + jest.spyOn(require('react-redux'), 'useDispatch'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders the component correctly, handles correct click event', () => { + const state = { + views: { + rhs: { + rhsState: '', + }, + }, + }; + const channel = TestHelper.getChannelMock(); + + renderWithContext( + + + , state, + ); + + const menuItem = screen.getByText('View Pinned Posts'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(rhsActions.showPinnedPosts).toHaveBeenCalledTimes(1); + expect(rhsActions.showPinnedPosts).toHaveBeenCalledWith(channel.id); + }); + + test('renders the component correctly, handles correct click event', () => { + const state = { + views: { + rhs: { + rhsState: RHSStates.PIN, + }, + }, + }; + const channel = TestHelper.getChannelMock(); + + renderWithContext( + + + , state, + ); + + const menuItem = screen.getByText('View Pinned Posts'); + expect(menuItem).toBeInTheDocument(); + + fireEvent.click(menuItem); // Simulate click on the menu item + expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called + expect(rhsActions.closeRightHandSide).toHaveBeenCalledTimes(1); + }); +}); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/view_pinned_posts.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/view_pinned_posts.tsx new file mode 100644 index 0000000000..8c0843794a --- /dev/null +++ b/webapp/channels/src/components/channel_header_menu/menu_items/view_pinned_posts.tsx @@ -0,0 +1,47 @@ +// 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 {closeRightHandSide, showPinnedPosts} from 'actions/views/rhs'; +import {getRhsState} from 'selectors/rhs'; + +import * as Menu from 'components/menu'; + +import {RHSStates} from 'utils/constants'; + +type Props = { + channelID: string; +} + +const ViewPinnedPosts = ({ + channelID, +}: Props) => { + const dispatch = useDispatch(); + const rhsState = useSelector(getRhsState); + const hasPinnedPosts = rhsState === RHSStates.PIN; + + const handleClick = () => { + if (hasPinnedPosts) { + dispatch(closeRightHandSide()); + } else { + dispatch(showPinnedPosts(channelID)); + } + }; + + return ( + + } + /> + ); +}; + +export default memo(ViewPinnedPosts); diff --git a/webapp/channels/src/components/channel_header_mobile/__snapshots__/channel_header_mobile.test.tsx.snap b/webapp/channels/src/components/channel_header_mobile/__snapshots__/channel_header_mobile.test.tsx.snap deleted file mode 100644 index 20cab4cc4c..0000000000 --- a/webapp/channels/src/components/channel_header_mobile/__snapshots__/channel_header_mobile.test.tsx.snap +++ /dev/null @@ -1,285 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot 1`] = ` - -`; - -exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, for default channel 1`] = ` - -`; - -exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, for private channel 1`] = ` - -`; - -exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, if DM channel 1`] = ` - -`; diff --git a/webapp/channels/src/components/channel_header_mobile/channel_header_mobile.test.tsx b/webapp/channels/src/components/channel_header_mobile/channel_header_mobile.test.tsx index 2bb83440c4..5d0d2927ba 100644 --- a/webapp/channels/src/components/channel_header_mobile/channel_header_mobile.test.tsx +++ b/webapp/channels/src/components/channel_header_mobile/channel_header_mobile.test.tsx @@ -1,9 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {shallow} from 'enzyme'; import React from 'react'; +import {renderWithContext, screen} from 'tests/react_testing_utils'; import {TestHelper} from 'utils/test_helper'; import ChannelHeaderMobile from './channel_header_mobile'; @@ -14,86 +14,91 @@ describe('components/ChannelHeaderMobile/ChannelHeaderMobile', () => { removeEventListener: jest.fn(), }); - const baseProps = { - user: TestHelper.getUserMock({ - id: 'user_id', - }), - channel: TestHelper.getChannelMock({ - type: 'O', - id: 'channel_id', - display_name: 'display_name', - team_id: 'team_id', - }), - member: TestHelper.getChannelMembershipMock({ - channel_id: 'channel_id', - user_id: 'user_id', - }), - teamDisplayName: 'team_display_name', - isPinnedPosts: true, - actions: { - closeLhs: jest.fn(), - closeRhs: jest.fn(), - closeRhsMenu: jest.fn(), - }, - isLicensed: true, - isMobileView: false, - isFavoriteChannel: false, + const user = TestHelper.getUserMock({ + id: 'user_id', + }); + const channel = TestHelper.getChannelMock({ + type: 'O', + id: 'channel_id', + display_name: 'display_name', + team_id: 'team_id', + }); + const actions = { + closeLhs: jest.fn(), + closeRhs: jest.fn(), + closeRhsMenu: jest.fn(), }; - test('should match snapshot', () => { - const wrapper = shallow( - , - ); + describe('components/ChannelHeaderMenu/MenuItem/ChannelHeaderMobile', () => { + test('renders the component correctly', () => { + renderWithContext( +
    + +
    , + ); - expect(wrapper).toMatchSnapshot(); - }); + let menuItem = screen.getByText('Toggle sidebar'); + expect(menuItem).toBeInTheDocument(); - test('should match snapshot, for default channel', () => { - const props = { - ...baseProps, - channel: TestHelper.getChannelMock({ - type: 'O', - id: '123', - name: 'town-square', - display_name: 'Town Square', - team_id: 'team_id', - }), - }; - const wrapper = shallow( - , - ); + menuItem = screen.getByLabelText('Info'); + expect(menuItem).toBeInTheDocument(); - expect(wrapper).toMatchSnapshot(); - }); + menuItem = screen.getByLabelText('Search'); + expect(menuItem).toBeInTheDocument(); - test('should match snapshot, if DM channel', () => { - const props = { - ...baseProps, - channel: TestHelper.getChannelMock({ - type: 'D', - id: 'channel_id', - name: 'user_id_1__user_id_2', - display_name: 'display_name', - team_id: 'team_id', - }), - }; - const wrapper = shallow(); + menuItem = screen.getByText('Toggle right sidebar'); + expect(menuItem).toBeInTheDocument(); - expect(wrapper).toMatchSnapshot(); - }); + const wrapper = screen.getByTestId('wrapper'); + expect(wrapper).toBeInTheDocument(); + }); - test('should match snapshot, for private channel', () => { - const props = { - ...baseProps, - channel: TestHelper.getChannelMock({ - type: 'P', - id: 'channel_id', - display_name: 'display_name', - team_id: 'team_id', - }), - }; - const wrapper = shallow(); + test('renders the component correctly, global threads', () => { + renderWithContext( +
    + +
    , + ); - expect(wrapper).toMatchSnapshot(); + const menuItem = screen.getByText('Followed threads'); + expect(menuItem).toBeInTheDocument(); + }); + + test('renders the component correctly, in drafts', () => { + renderWithContext( +
    + +
    , + ); + + const menuItem = screen.getByText('Drafts'); + expect(menuItem).toBeInTheDocument(); + }); }); }); diff --git a/webapp/channels/src/components/channel_header_mobile/channel_header_mobile.tsx b/webapp/channels/src/components/channel_header_mobile/channel_header_mobile.tsx index a52da2531e..0f647f0a38 100644 --- a/webapp/channels/src/components/channel_header_mobile/channel_header_mobile.tsx +++ b/webapp/channels/src/components/channel_header_mobile/channel_header_mobile.tsx @@ -8,29 +8,22 @@ import {FormattedMessage} from 'react-intl'; import type {Channel} from '@mattermost/types/channels'; import type {UserProfile} from '@mattermost/types/users'; -import {MobileChannelHeaderDropdown} from 'components/channel_header_dropdown'; - -import MobileChannelHeaderPlug from 'plugins/mobile_channel_header_plug'; - import ChannelInfoButton from './channel_info_button'; import CollapseLhsButton from './collapse_lhs_button'; import CollapseRhsButton from './collapse_rhs_button'; import ShowSearchButton from './show_search_button'; import UnmuteChannelButton from './unmute_channel_button'; +import ChannelHeaderMenu from '../channel_header_menu/channel_header_menu'; +import MobileChannelHeaderPlugins from '../channel_header_menu/menu_items/mobile_channel_header_plugins'; + type Props = { channel?: Channel; - /** - * Relative url for the team, used to redirect if a link in the channel header is clicked - */ - currentRelativeTeamUrl?: string; - inGlobalThreads?: boolean; inDrafts?: boolean; isMobileView: boolean; isMuted?: boolean; - isReadOnly?: boolean; isRHSOpen?: boolean; user: UserProfile; actions: { @@ -85,7 +78,10 @@ export default class ChannelHeaderMobile extends React.PureComponent { } else if (channel) { heading = ( <> - + + {isMuted && ( { channel={channel} /> )} - {channel && ( - )} + diff --git a/webapp/channels/src/components/channel_header_mobile/collapse_rhs_button/collapse_rhs_button.tsx b/webapp/channels/src/components/channel_header_mobile/collapse_rhs_button/collapse_rhs_button.tsx index 1c6e6c51bf..e2dd4f8ae0 100644 --- a/webapp/channels/src/components/channel_header_mobile/collapse_rhs_button/collapse_rhs_button.tsx +++ b/webapp/channels/src/components/channel_header_mobile/collapse_rhs_button/collapse_rhs_button.tsx @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; +import {FormattedMessage} from 'react-intl'; import MenuIcon from 'components/widgets/icons/menu_icon'; @@ -22,6 +23,12 @@ const CollapseRhsButton: React.FunctionComponent = (props: Props) => ( data-target='#sidebar-nav' onClick={props.actions.toggleRhsMenu} > + + + ); diff --git a/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.tsx b/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.tsx index 351b9aae2f..e6fa5bff6e 100644 --- a/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.tsx +++ b/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.tsx @@ -62,7 +62,7 @@ const ConvertGmToChannelModal = (props: Props) => { map((user) => displayUsername(user, props.teammateNameDisplaySetting)); setChannelMemberNames(validProfilesInChannel); - }, [props.profilesInChannel]); + }, [props.profilesInChannel, props.currentUserId, props.teammateNameDisplaySetting]); const [commonTeamsById, setCommonTeamsById] = useState<{[id: string]: Team}>({}); const [commonTeamsFetched, setCommonTeamsFetched] = useState(false); @@ -109,7 +109,7 @@ const ConvertGmToChannelModal = (props: Props) => { work(); setTimeout(() => setLoadingAnimationTimeout(true), 1200); - }, []); + }, [dispatch, props.channel.id]); const handleConfirm = useCallback(async () => { if (!selectedTeamId) { @@ -136,7 +136,7 @@ const ConvertGmToChannelModal = (props: Props) => { setConversionError(undefined); trackEvent('actions', 'convert_group_message_to_private_channel', {channel_id: props.channel.id}); props.onExited(); - }, [selectedTeamId, props.channel.id, channelName, channelURL.current, props.actions.moveChannelsInSidebar]); + }, [selectedTeamId, props, channelName, formatMessage]); const showLoader = !commonTeamsFetched || !loadingAnimationTimeout; const canCreate = selectedTeamId !== undefined && channelName !== '' && !nameError && !urlError; diff --git a/webapp/channels/src/components/convert_gm_to_channel_modal/index.ts b/webapp/channels/src/components/convert_gm_to_channel_modal/index.ts index d71da3cb24..7aeb517803 100644 --- a/webapp/channels/src/components/convert_gm_to_channel_modal/index.ts +++ b/webapp/channels/src/components/convert_gm_to_channel_modal/index.ts @@ -5,6 +5,8 @@ import {connect} from 'react-redux'; import type {Dispatch} from 'redux'; import {bindActionCreators} from 'redux'; +import type {Channel} from '@mattermost/types/channels'; + import {convertGroupMessageToPrivateChannel} from 'mattermost-redux/actions/channels'; import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences'; import { @@ -16,15 +18,17 @@ import type {ActionResult} from 'mattermost-redux/types/actions'; import {moveChannelsInSidebar} from 'actions/views/channel_sidebar'; import {closeModal} from 'actions/views/modals'; -import type {Props} from 'components/convert_gm_to_channel_modal/convert_gm_to_channel_modal'; -import ConvertGmToChannelModal from 'components/convert_gm_to_channel_modal/convert_gm_to_channel_modal'; - import type {GlobalState} from 'types/store'; +import ConvertGmToChannelModal from './convert_gm_to_channel_modal'; + +type OwnProps = { + channel: Channel; +} function makeMapStateToProps() { const getProfilesInChannel = makeGetProfilesInChannel(); - return (state: GlobalState, ownProps: Props) => { + return (state: GlobalState, ownProps: OwnProps) => { const allProfilesInChannel = getProfilesInChannel(state, ownProps.channel.id); const currentUserId = getCurrentUserId(state); const teammateNameDisplaySetting = getTeammateNameDisplaySetting(state); diff --git a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/__snapshots__/sidebar_channel_menu.test.tsx.snap b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/__snapshots__/sidebar_channel_menu.test.tsx.snap index 067f2bffcf..029465220f 100644 --- a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/__snapshots__/sidebar_channel_menu.test.tsx.snap +++ b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/__snapshots__/sidebar_channel_menu.test.tsx.snap @@ -495,8 +495,8 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should show cor id="mute-channel_id" labels={ } leadingElement={ diff --git a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/sidebar_channel_menu.tsx b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/sidebar_channel_menu.tsx index 14c4ace952..35b0c14354 100644 --- a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/sidebar_channel_menu.tsx +++ b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_menu/sidebar_channel_menu.tsx @@ -148,8 +148,8 @@ const SidebarChannelMenu = ({ if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) { muteChannelText = ( ); } @@ -176,8 +176,8 @@ const SidebarChannelMenu = ({ if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) { muteChannelText = ( ); } diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 212ddf860e..490da24b5a 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -3230,22 +3230,22 @@ "channel_header.lastActive": "Last online {timestamp}", "channel_header.lastOnline": "Last online {timestamp}", "channel_header.leave": "Leave Channel", - "channel_header.manageMembers": "Manage Members", - "channel_header.menuAriaLabel": "Channel Menu", + "channel_header.members": "Members", "channel_header.mute": "Mute Channel", - "channel_header.muteConversation": "Mute Conversation", + "channel_header.muteConversation": "Mute", "channel_header.openChannelInfo": "View Info", "channel_header.otherchannel": "{displayName} Channel Menu", "channel_header.pinnedPosts": "Pinned messages", "channel_header.recentMentions": "Recent mentions", "channel_header.rename": "Rename Channel", "channel_header.search": "Search", - "channel_header.setConversationHeader": "Edit Conversation Header", + "channel_header.setConversationHeader": "Edit Header", "channel_header.setHeader": "Edit Channel Header", "channel_header.setPurpose": "Edit Channel Purpose", + "channel_header.settings": "Settings", "channel_header.unarchive": "Unarchive Channel", "channel_header.unmute": "Unmute Channel", - "channel_header.unmuteConversation": "Unmute Conversation", + "channel_header.unmuteConversation": "Unmute", "channel_header.userHelpGuide": "Help", "channel_header.viewMembers": "View Members", "channel_info_rhs.about_area_id": "ID:", @@ -3330,6 +3330,9 @@ "channel_members_rhs.member.send_message": "Send message", "channel_members_rhs.search_bar.aria.cancel_search_button": "cancel members search", "channel_members_rhs.search_bar.placeholder": "Search members", + "channel_menu.bookmarks": "Bookmarks Bar", + "channel_menu.bookmarks.addFile": "Attach a file", + "channel_menu.bookmarks.addLink": "Add a link", "channel_modal.alreadyExist": "A channel with that URL already exists", "channel_modal.cancel": "Cancel", "channel_modal.create_board.tooltip_description": "Use any of our templates to manage your tasks or start from scratch with your own!", @@ -3390,6 +3393,7 @@ "channelNotifications.mobileNotification.newMessages": "All new messages {optionalDefault}", "channelNotifications.mobileNotification.nothing": "Nothing {optionalDefault}", "channelSelectorModal.title": "Add Channels to Channel Selection List", + "channelSettings": "Channel Settings", "channelView.archivedChannel": "You are viewing an archived channel. New messages cannot be posted.", "channelView.archivedChannelWithDeactivatedUser": "You are viewing an archived channel with a deactivated user. New messages cannot be posted.", "channelView.login.successfull": "Login Successful", @@ -4499,6 +4503,7 @@ "navbar.addMembers": "Add Members", "navbar.preferences": "Notification Preferences", "navbar.toggle2": "Toggle sidebar", + "navbar.toggle3": "Toggle right sidebar", "navbar.viewPinnedPosts": "View Pinned Posts", "newChannelWithBoard.tutorialTip.description": "The board you just created can be quickly accessed by clicking on the Boards icon in the App bar. You can view the boards that are linked to this channel in the right-hand sidebar and open one in full view.", "newChannelWithBoard.tutorialTip.title": "Access linked boards from the App Bar", @@ -4662,6 +4667,7 @@ "plan.self_serve": "Self-serve", "pluggable.errorOccurred": "An error occurred in the {pluginId} plugin.", "pluggable.errorRefresh": "Refresh?", + "pluginsMenu.more_actions": "More actions", "post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They cannot be added to the channel because they are not a member of the linked groups. To add them to this channel, they must be added to the linked groups.", "post_body.check_for_out_of_channel_mentions.link.and": " and ", "post_body.check_for_out_of_channel_mentions.link.private": "add them to this private channel", @@ -5183,7 +5189,6 @@ "sidebar_left.sidebar_category.newLabel": "new", "sidebar_left.sidebar_channel_menu_convert_to_channel": "Convert to Private Channel", "sidebar_left.sidebar_channel_menu.addMembers": "Add Members", - "sidebar_left.sidebar_channel_menu.bookmarks": "Bookmarks Bar", "sidebar_left.sidebar_channel_menu.channels": "Channels", "sidebar_left.sidebar_channel_menu.copyLink": "Copy Link", "sidebar_left.sidebar_channel_menu.dropdownAriaLabel": "Edit channel menu", @@ -5198,11 +5203,11 @@ "sidebar_left.sidebar_channel_menu.moveTo": "Move to...", "sidebar_left.sidebar_channel_menu.moveTo.dropdownAriaLabel": "Move to submenu", "sidebar_left.sidebar_channel_menu.moveToNewCategory": "New Category", + "sidebar_left.sidebar_channel_menu.mute": "Mute", "sidebar_left.sidebar_channel_menu.muteChannel": "Mute Channel", - "sidebar_left.sidebar_channel_menu.muteConversation": "Mute Conversation", "sidebar_left.sidebar_channel_menu.unfavoriteChannel": "Unfavorite", + "sidebar_left.sidebar_channel_menu.unmute": "Unmute", "sidebar_left.sidebar_channel_menu.unmuteChannel": "Unmute Channel", - "sidebar_left.sidebar_channel_menu.unmuteConversation": "Unmute Conversation", "sidebar_left.sidebar_channel_modal.channel_name_placeholder": "Enter a name for the channel", "sidebar_left.sidebar_channel_modal.confirmation_text": "Convert to private channel", "sidebar_left.sidebar_channel_modal.header": "Convert to Private Channel", diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/channels.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/channels.ts index b87a97094e..f1a0cb962b 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/channels.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/channels.ts @@ -1234,6 +1234,11 @@ export const getMyFirstChannelForTeams: (state: GlobalState) => RelationOneToOne }, ); +export const getRedirectChannelNameForCurrentTeam = (state: GlobalState): string => { + const currentTeamId = getCurrentTeamId(state); + return getRedirectChannelNameForTeam(state, currentTeamId); +}; + export const getRedirectChannelNameForTeam = (state: GlobalState, teamId: string): string => { const defaultChannelForTeam = getDefaultChannelForTeams(state)[teamId]; const canIJoinPublicChannelsInTeam = haveITeamPermission(state, diff --git a/webapp/channels/src/plugins/mobile_channel_header_plug/__snapshots__/mobile_channel_header_plug.test.tsx.snap b/webapp/channels/src/plugins/mobile_channel_header_plug/__snapshots__/mobile_channel_header_plug.test.tsx.snap deleted file mode 100644 index 81c46341e7..0000000000 --- a/webapp/channels/src/plugins/mobile_channel_header_plug/__snapshots__/mobile_channel_header_plug.test.tsx.snap +++ /dev/null @@ -1,1017 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with no binding, in dropdown 1`] = ` - -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with no bindings 1`] = ` - -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with no extended component 1`] = ` - -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with no extended component, in dropdown 1`] = ` - -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with one binding 1`] = ` - -
  • - -
  • -
    -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with one binding, in dropdown 1`] = ` - -
  • - - Label - -
  • -
    -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with one extended component 1`] = ` -, - "id": "someid", - "pluginId": "pluginid", - }, - ] - } - intl={ - Object { - "formatMessage": [Function], - } - } - isDropdown={false} - theme={Object {}} -> -
  • - -
  • -
    -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with one extended component and one binding, in dropdown 1`] = ` -, - "id": "someid", - "pluginId": "pluginid", - }, - ] - } - intl={ - Object { - "$t": [Function], - "defaultFormats": Object {}, - "defaultLocale": "en", - "defaultRichTextElements": undefined, - "fallbackOnEmptyString": true, - "formatDate": [Function], - "formatDateTimeRange": [Function], - "formatDateToParts": [Function], - "formatDisplayName": [Function], - "formatList": [Function], - "formatListToParts": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatNumberToParts": [Function], - "formatPlural": [Function], - "formatRelativeTime": [Function], - "formatTime": [Function], - "formatTimeToParts": [Function], - "formats": Object {}, - "formatters": Object { - "getDateTimeFormat": [Function], - "getDisplayNames": [Function], - "getListFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralRules": [Function], - "getRelativeTimeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "onError": [Function], - "onWarn": [Function], - "textComponent": "span", - "timeZone": "Etc/UTC", - } - } - isDropdown={true} - theme={Object {}} -> -
  • - - some dropdown text - -
  • -
  • - - Label - -
  • -
    -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with one extended component, in dropdown 1`] = ` -, - "id": "someid", - "pluginId": "pluginid", - }, - ] - } - intl={ - Object { - "$t": [Function], - "defaultFormats": Object {}, - "defaultLocale": "en", - "defaultRichTextElements": undefined, - "fallbackOnEmptyString": true, - "formatDate": [Function], - "formatDateTimeRange": [Function], - "formatDateToParts": [Function], - "formatDisplayName": [Function], - "formatList": [Function], - "formatListToParts": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatNumberToParts": [Function], - "formatPlural": [Function], - "formatRelativeTime": [Function], - "formatTime": [Function], - "formatTimeToParts": [Function], - "formats": Object {}, - "formatters": Object { - "getDateTimeFormat": [Function], - "getDisplayNames": [Function], - "getListFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralRules": [Function], - "getRelativeTimeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "onError": [Function], - "onWarn": [Function], - "textComponent": "span", - "timeZone": "Etc/UTC", - } - } - isDropdown={true} - theme={Object {}} -> -
  • - - some dropdown text - -
  • -
    -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with one extended components and one binding 1`] = ` -, - "id": "someid", - "pluginId": "pluginid", - }, - ] - } - intl={ - Object { - "$t": [Function], - "defaultFormats": Object {}, - "defaultLocale": "en", - "defaultRichTextElements": undefined, - "fallbackOnEmptyString": true, - "formatDate": [Function], - "formatDateTimeRange": [Function], - "formatDateToParts": [Function], - "formatDisplayName": [Function], - "formatList": [Function], - "formatListToParts": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatNumberToParts": [Function], - "formatPlural": [Function], - "formatRelativeTime": [Function], - "formatTime": [Function], - "formatTimeToParts": [Function], - "formats": Object {}, - "formatters": Object { - "getDateTimeFormat": [Function], - "getDisplayNames": [Function], - "getListFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralRules": [Function], - "getRelativeTimeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "onError": [Function], - "onWarn": [Function], - "textComponent": "span", - "timeZone": "Etc/UTC", - } - } - isDropdown={false} - theme={Object {}} -/> -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with two bindings 1`] = ` - -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with two bindings, in dropdown 1`] = ` - -
  • - - Label - -
  • -
  • - - Label - -
  • -
    -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with two extended components 1`] = ` -, - "id": "someid", - "pluginId": "pluginid", - }, - Object { - "action": [MockFunction], - "dropdownText": "some dropdown text", - "icon": , - "id": "someid2", - "pluginId": "pluginid", - }, - ] - } - intl={ - Object { - "$t": [Function], - "defaultFormats": Object {}, - "defaultLocale": "en", - "defaultRichTextElements": undefined, - "fallbackOnEmptyString": true, - "formatDate": [Function], - "formatDateTimeRange": [Function], - "formatDateToParts": [Function], - "formatDisplayName": [Function], - "formatList": [Function], - "formatListToParts": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatNumberToParts": [Function], - "formatPlural": [Function], - "formatRelativeTime": [Function], - "formatTime": [Function], - "formatTimeToParts": [Function], - "formats": Object {}, - "formatters": Object { - "getDateTimeFormat": [Function], - "getDisplayNames": [Function], - "getListFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralRules": [Function], - "getRelativeTimeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "onError": [Function], - "onWarn": [Function], - "textComponent": "span", - "timeZone": "Etc/UTC", - } - } - isDropdown={false} - theme={Object {}} -/> -`; - -exports[`plugins/MobileChannelHeaderPlug should match snapshot with two extended components, in dropdown 1`] = ` -, - "id": "someid", - "pluginId": "pluginid", - }, - Object { - "action": [MockFunction], - "dropdownText": "some dropdown text", - "icon": , - "id": "someid2", - "pluginId": "pluginid", - }, - ] - } - intl={ - Object { - "formatMessage": [Function], - } - } - isDropdown={true} - theme={Object {}} -> -
  • - - some dropdown text - -
  • -
  • - - some dropdown text - -
  • -
    -`; diff --git a/webapp/channels/src/plugins/mobile_channel_header_plug/index.ts b/webapp/channels/src/plugins/mobile_channel_header_plug/index.ts deleted file mode 100644 index 9422546e6d..0000000000 --- a/webapp/channels/src/plugins/mobile_channel_header_plug/index.ts +++ /dev/null @@ -1,42 +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 {AppBindingLocations} from 'mattermost-redux/constants/apps'; -import {appsEnabled, makeAppBindingsSelector} from 'mattermost-redux/selectors/entities/apps'; -import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; - -import {handleBindingClick, openAppsModal, postEphemeralCallResponseForChannel} from 'actions/apps'; - -import type {GlobalState} from 'types/store'; - -import MobileChannelHeaderPlug from './mobile_channel_header_plug'; - -const getChannelHeaderBindings = makeAppBindingsSelector(AppBindingLocations.CHANNEL_HEADER_ICON); - -function mapStateToProps(state: GlobalState) { - const apps = appsEnabled(state); - return { - appBindings: getChannelHeaderBindings(state), - appsEnabled: apps, - channelMember: getMyCurrentChannelMembership(state), - components: state.plugins.components.MobileChannelHeaderButton, - theme: getTheme(state), - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - actions: bindActionCreators({ - handleBindingClick, - postEphemeralCallResponseForChannel, - openAppsModal, - }, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(MobileChannelHeaderPlug); diff --git a/webapp/channels/src/plugins/mobile_channel_header_plug/mobile_channel_header_plug.test.tsx b/webapp/channels/src/plugins/mobile_channel_header_plug/mobile_channel_header_plug.test.tsx deleted file mode 100644 index 40421565f2..0000000000 --- a/webapp/channels/src/plugins/mobile_channel_header_plug/mobile_channel_header_plug.test.tsx +++ /dev/null @@ -1,473 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {mount} from 'enzyme'; -import React from 'react'; - -import type {Channel, ChannelMembership} from '@mattermost/types/channels'; - -import {AppCallResponseTypes} from 'mattermost-redux/constants/apps'; -import type {Theme} from 'mattermost-redux/selectors/entities/preferences'; - -import MobileChannelHeaderPlug, {RawMobileChannelHeaderPlug} from 'plugins/mobile_channel_header_plug/mobile_channel_header_plug'; -import {mountWithIntl} from 'tests/helpers/intl-test-helper'; -import {createCallContext} from 'utils/apps'; - -describe('plugins/MobileChannelHeaderPlug', () => { - const testPlug = { - id: 'someid', - pluginId: 'pluginid', - icon: , - action: jest.fn(), - dropdownText: 'some dropdown text', - }; - - const testBinding = { - app_id: 'appid', - location: 'test', - icon: 'http://test.com/icon.png', - label: 'Label', - hint: 'Hint', - form: { - submit: { - path: '/call/path', - }, - }, - }; - - const testChannel = {} as Channel; - const testChannelMember = {} as ChannelMembership; - const testTheme = {} as Theme; - const intl = { - formatMessage: (message: {id: string; defaultMessage: string}) => { - return message.defaultMessage; - }, - } as any; - - test('should match snapshot with no extended component', () => { - const wrapper = mountWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render nothing - expect(wrapper.find('li').exists()).toBe(false); - }); - - test('should match snapshot with one extended component', () => { - const wrapper = mount( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render a single list item containing a button - expect(wrapper.find('li')).toHaveLength(1); - expect(wrapper.find('button')).toHaveLength(1); - - wrapper.instance().fireAction = jest.fn(); - wrapper.find('button').first().simulate('click'); - expect(wrapper.instance().fireAction).toHaveBeenCalledTimes(1); - expect(wrapper.instance().fireAction).toBeCalledWith(testPlug); - }); - - test('should match snapshot with two extended components', () => { - const wrapper = mountWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render nothing - expect(wrapper.find('li').exists()).toBe(false); - }); - - test('should match snapshot with no bindings', () => { - const wrapper = mountWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render nothing - expect(wrapper.find('li').exists()).toBe(false); - }); - - test('should match snapshot with one binding', () => { - const wrapper = mount( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render a single list item containing a button - expect(wrapper.find('li')).toHaveLength(1); - expect(wrapper.find('button')).toHaveLength(1); - - wrapper.instance().fireAppAction = jest.fn(); - wrapper.find('button').first().simulate('click'); - expect(wrapper.instance().fireAppAction).toHaveBeenCalledTimes(1); - expect(wrapper.instance().fireAppAction).toBeCalledWith(testBinding); - }); - - test('should match snapshot with two bindings', () => { - const wrapper = mountWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render nothing - expect(wrapper.find('li').exists()).toBe(false); - }); - - test('should match snapshot with one extended components and one binding', () => { - const wrapper = mountWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render nothing - expect(wrapper.find('li').exists()).toBe(false); - }); - - test('should match snapshot with no extended component, in dropdown', () => { - const wrapper = mountWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render nothing - expect(wrapper.find('li').exists()).toBe(false); - }); - - test('should match snapshot with one extended component, in dropdown', () => { - const wrapper = mountWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render a single list item containing an anchor - expect(wrapper.find('li')).toHaveLength(1); - expect(wrapper.find('a')).toHaveLength(1); - }); - - test('should match snapshot with two extended components, in dropdown', () => { - const wrapper = mount( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render a two list items containing anchors - expect(wrapper.find('li')).toHaveLength(2); - expect(wrapper.find('a')).toHaveLength(2); - - const instance = wrapper.instance(); - instance.fireAction = jest.fn(); - - wrapper.find('a').first().simulate('click'); - expect(instance.fireAction).toHaveBeenCalledTimes(1); - expect(instance.fireAction).toBeCalledWith(testPlug); - }); - - test('should match snapshot with no binding, in dropdown', () => { - const wrapper = mountWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render nothing - expect(wrapper.find('li').exists()).toBe(false); - }); - - test('should match snapshot with one binding, in dropdown', () => { - const wrapper = mountWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render a single list item containing an anchor - expect(wrapper.find('li')).toHaveLength(1); - expect(wrapper.find('a')).toHaveLength(1); - }); - - test('should match snapshot with two bindings, in dropdown', () => { - const wrapper = mount( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render a two list items containing anchors - expect(wrapper.find('li')).toHaveLength(2); - expect(wrapper.find('a')).toHaveLength(2); - - const instance = wrapper.instance(); - instance.fireAppAction = jest.fn(); - - wrapper.find('a').first().simulate('click'); - expect(instance.fireAppAction).toHaveBeenCalledTimes(1); - expect(instance.fireAppAction).toBeCalledWith(testBinding); - }); - - test('should match snapshot with one extended component and one binding, in dropdown', () => { - const wrapper = mountWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - - // Render a two list items containing anchors - expect(wrapper.find('li')).toHaveLength(2); - expect(wrapper.find('a')).toHaveLength(2); - }); - - test('should call plugin.action on fireAction', () => { - const channel = {id: 'channel_id'} as Channel; - const channelMember = {} as ChannelMembership; - const newTestPlug = { - id: 'someid', - pluginId: 'pluginid', - icon: , - action: jest.fn(), - dropdownText: 'some dropdown text', - }; - - const wrapper = mount( - , - ); - - wrapper.instance().fireAction(newTestPlug); - expect(newTestPlug.action).toHaveBeenCalledTimes(1); - expect(newTestPlug.action).toBeCalledWith(channel, channelMember); - }); - - test('should call handleBindingClick on fireAppAction', () => { - const channel = {id: 'channel_id'} as Channel; - const channelMember = {} as ChannelMembership; - - const handleBindingClick = jest.fn().mockResolvedValue({data: {type: AppCallResponseTypes.OK}}); - - const wrapper = mount( - , - ); - - const context = createCallContext( - testBinding.app_id, - testBinding.location, - channel.id, - channel.team_id, - ); - - wrapper.instance().fireAppAction(testBinding); - expect(handleBindingClick).toHaveBeenCalledTimes(1); - expect(handleBindingClick).toBeCalledWith(testBinding, context, expect.anything()); - }); -}); diff --git a/webapp/channels/src/plugins/mobile_channel_header_plug/mobile_channel_header_plug.tsx b/webapp/channels/src/plugins/mobile_channel_header_plug/mobile_channel_header_plug.tsx deleted file mode 100644 index d5bbed6452..0000000000 --- a/webapp/channels/src/plugins/mobile_channel_header_plug/mobile_channel_header_plug.tsx +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {injectIntl} from 'react-intl'; -import type {IntlShape} from 'react-intl'; - -import type {AppBinding} from '@mattermost/types/apps'; -import type {Channel, ChannelMembership} from '@mattermost/types/channels'; - -import {AppCallResponseTypes} from 'mattermost-redux/constants/apps'; -import type {Theme} from 'mattermost-redux/selectors/entities/preferences'; - -import {createCallContext} from 'utils/apps'; - -import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForChannel} from 'types/apps'; -import type {MobileChannelHeaderButtonAction} from 'types/store/plugins'; - -type Props = { - - /* - * Components or actions to add as channel header buttons - */ - components?: MobileChannelHeaderButtonAction[]; - - /* - * Set to true if the plug is in the dropdown - */ - isDropdown: boolean; - channel: Channel; - channelMember?: ChannelMembership; - - /* - * Logged in user's theme - */ - theme: Theme; - appBindings: AppBinding[]; - appsEnabled: boolean; - intl: IntlShape; - actions: { - handleBindingClick: HandleBindingClick; - postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel; - openAppsModal: OpenAppsModal; - }; -} - -class MobileChannelHeaderPlug extends React.PureComponent { - createAppButton = (binding: AppBinding) => { - const onClick = () => this.fireAppAction(binding); - - if (this.props.isDropdown) { - return ( -
  • - - {binding.label} - -
  • - ); - } - - return ( -
  • - -
  • - ); - }; - createButton = (plug: MobileChannelHeaderButtonAction) => { - const onClick = () => this.fireAction(plug); - - if (this.props.isDropdown) { - return ( -
  • - - {plug.dropdownText} - -
  • - ); - } - - return ( -
  • - -
  • - ); - }; - - createList(plugs: MobileChannelHeaderButtonAction[]) { - return plugs.map(this.createButton); - } - - createAppList(bindings: AppBinding[]) { - return bindings.map(this.createAppButton); - } - - fireAction(plug: MobileChannelHeaderButtonAction) { - return plug.action?.(this.props.channel, this.props.channelMember); - } - - fireAppAction = async (binding: AppBinding) => { - const {channel, intl} = this.props; - const context = createCallContext( - binding.app_id, - binding.location, - channel.id, - channel.team_id, - ); - - const res = await this.props.actions.handleBindingClick(binding, context, intl); - - if (res.error) { - const errorResponse = res.error; - const errorMessage = errorResponse.text || intl.formatMessage({ - id: 'apps.error.unknown', - defaultMessage: 'Unknown error occurred.', - }); - this.props.actions.postEphemeralCallResponseForChannel(errorResponse, errorMessage, channel.id); - return; - } - - const callResp = res.data!; - switch (callResp.type) { - case AppCallResponseTypes.OK: - if (callResp.text) { - this.props.actions.postEphemeralCallResponseForChannel(callResp, callResp.text, channel.id); - } - break; - case AppCallResponseTypes.NAVIGATE: - break; - case AppCallResponseTypes.FORM: - if (callResp.form) { - this.props.actions.openAppsModal(callResp.form, context); - } - break; - default: { - const errorMessage = this.props.intl.formatMessage({ - id: 'apps.error.responses.unknown_type', - defaultMessage: 'App response type not supported. Response type: {type}.', - }, { - type: callResp.type, - }); - this.props.actions.postEphemeralCallResponseForChannel(callResp, errorMessage, channel.id); - } - } - }; - - render() { - const components = this.props.components || []; - const bindings = this.props.appBindings || []; - - if (components.length === 0 && bindings.length === 0) { - return null; - } else if (components.length === 1 && bindings.length === 0) { - return this.createButton(components[0]); - } else if (components.length === 0 && bindings.length === 1) { - return this.createAppButton(bindings[0]); - } - - if (!this.props.isDropdown) { - return null; - } - - const plugItems = this.createList(components); - const appItems = this.createAppList(bindings); - return (<> - {plugItems} - {appItems} - ); - } -} - -// Exported for tests -export {MobileChannelHeaderPlug as RawMobileChannelHeaderPlug}; - -export default injectIntl(MobileChannelHeaderPlug); diff --git a/webapp/channels/src/sass/responsive/_mobile.scss b/webapp/channels/src/sass/responsive/_mobile.scss index 1b302a265f..aaac4d0464 100644 --- a/webapp/channels/src/sass/responsive/_mobile.scss +++ b/webapp/channels/src/sass/responsive/_mobile.scss @@ -35,100 +35,6 @@ flex: 1; } - .Menu { - position: fixed; - top: 48px; - left: 0; - display: block; - overflow: hidden; - width: 100%; - height: 100%; - padding: 2.5em 0 0; - padding-bottom: 50px; - background: functions.alpha-color(variables.$black, 0.9); - transition: all 0.35s ease; - - body.announcement-bar--fixed & { - top: calc(48px + #{variables.$announcement-bar-height}); - height: calc(100% - 48px - #{variables.$announcement-bar-height}); - } - } - - .Menu__content { - position: absolute; - top: 0; - overflow: auto; - width: 100%; - max-width: 100%; - height: 100%; - max-height: 100%; - padding: 2rem 2rem 5rem; - background: transparent; - } - - .Menu__close { - position: fixed; - top: 70px; - right: 20px; - display: block; - width: 30px; - height: 30px; - padding-top: 13px; - border: 1px solid variables.$white; - border-radius: 50%; - margin-left: -15px; - color: variables.$white; - font-family: 'Open Sans', sans-serif; - font-size: 23px; - font-weight: 200; - line-height: 0; - opacity: 1; - text-align: center; - text-shadow: none; - } - - .dropdown-menu { - .dropdown__divider { - & + .dropdown__divider { - display: none; - } - } - - >li { - >a, - >button, - >.styleSelectableItemDiv { - display: flex; - width: 100%; - height: auto; - justify-content: center; - padding: 0.4rem 2rem; - margin: 0 auto; - color: variables.$white; - font-weight: 600; - line-height: 32px; - text-align: center; - - &:hover { - background: transparent; - } - - .SubMenu__icon-right { - color: rgba(255, 255, 255, 0.75); - } - } - - .navbar-right__icon { - margin: 10px; - } - - hr { - border-bottom: 1px solid functions.alpha-color(variables.$white, 0.3); - margin: 8px auto; - } - } - } - .status { top: 2px; width: 16px; @@ -559,24 +465,6 @@ &.dropdown-menu { max-width: 100%; } - - &.mobile-channel-header-dropdown-enter { - transform: translateY(100%); - } - - &.mobile-channel-header-dropdown-enter-active { - transform: translateY(0%); - transition: transform 0.35s ease-in; - } - - &.mobile-channel-header-dropdown-enter-done { - transform: translateY(0%); - } - - &.mobile-channel-header-dropdown-exit { - transform: translateY(100%); - transition: transform 0.35s ease-in; - } } } @@ -2175,24 +2063,6 @@ } } -@media screen and (max-width: 380px) and (max-height: 580px) { - #navbar_wrapper { - .navbar-default { - .dropdown-menu { - padding-top: 1em; - - >li { - >a { - border: none; - font-size: 13px; - line-height: 27px; - } - } - } - } - } -} - // on iOS, allow clicks within an input's label to actually propagate through to the input itself, // but still allow clicks to a elements to go trough // http://stackoverflow.com/a/34810294/6325807 diff --git a/webapp/channels/src/selectors/plugins.test.js b/webapp/channels/src/selectors/plugins.test.js index cb12245c8f..8d4107ab1c 100644 --- a/webapp/channels/src/selectors/plugins.test.js +++ b/webapp/channels/src/selectors/plugins.test.js @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {getChannelHeaderMenuPluginComponents, getPluginUserSettings} from 'selectors/plugins'; +import {getChannelHeaderMenuPluginComponents, getPluginUserSettings, getChannelMobileHeaderPluginButtons} from 'selectors/plugins'; describe('selectors/plugins', () => { describe('getPluginUserSettings', () => { @@ -207,4 +207,34 @@ describe('selectors/plugins', () => { ]); }); }); + + describe('getChannelMobileHeaderPluginButtons', () => { + it('has no settings', () => { + const state = { + plugins: { + components: { + MobileChannelHeaderButton: [], + }, + }, + }; + const settings = getChannelMobileHeaderPluginButtons(state); + expect(settings).toEqual([]); + }); + it('has settings', () => { + const headerButton = { + id: 'someid', + pluginId: 'pluginid', + dropdownText: 'some dropdown text', + }; + const state = { + plugins: { + components: { + MobileChannelHeaderButton: [headerButton], + }, + }, + }; + const settings = getChannelMobileHeaderPluginButtons(state); + expect(settings).toEqual([headerButton]); + }); + }); }); diff --git a/webapp/channels/src/selectors/plugins.ts b/webapp/channels/src/selectors/plugins.ts index 770d8722fa..7b0f24ae5f 100644 --- a/webapp/channels/src/selectors/plugins.ts +++ b/webapp/channels/src/selectors/plugins.ts @@ -74,6 +74,14 @@ export const getChannelHeaderMenuPluginComponents = createShallowSelector( }, ); +export const getChannelMobileHeaderPluginButtons = createSelector( + 'getChannelMobileHeaderPluginButtons', + (state: GlobalState) => state.plugins.components.MobileChannelHeaderButton, + (components = []) => { + return components; + }, +); + export const getChannelIntroPluginButtons = createSelector( 'getChannelIntroPluginButtons', (state: GlobalState) => state.plugins.components.ChannelIntroButton,