From 4b6216f5a4e3c7e74c7359591d88c0bc33ed50a9 Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Wed, 15 Jan 2025 00:55:38 +0530 Subject: [PATCH] [MM-55274] User account menu doesn't follow standard accessible keyboard behavior (#29173) --- .../accessibility/accessibility_image_spec.js | 6 +- .../account_settings/profile/email_spec.ts | 5 +- .../account_settings/profile/username_spec.ts | 4 +- .../join_archived_channel_spec.ts | 2 +- .../sidebar_category_menu_spec.ts | 6 + .../custom_status/custom_status_2_spec.ts | 26 +- .../custom_status/custom_status_3_spec.ts | 28 +- .../custom_status/custom_status_4_spec.ts | 2 +- .../custom_status/custom_status_5_spec.ts | 2 +- .../custom_status_expiry_1_spec.ts | 55 +- .../custom_status_expiry_2_spec.ts | 78 +- .../custom_status_expiry_3_spec.ts | 6 +- .../custom_status_expiry_4_spec.ts | 6 +- .../user_status_commands_spec.js | 10 +- .../builtin_commands/user_status_spec.js | 15 +- .../channels/menus/main_menu_spec.js | 7 +- .../channels/menus/status_dropdown_spec.js | 44 +- ...th_same_firstname_channel_mentions_spec.js | 2 +- .../profile_settings/profile_settings_spec.js | 4 +- .../theme/custom_theme_sidebar_styles_spec.js | 8 +- .../login_logout_smoke_spec.js | 2 +- .../login_open_server_spec.js | 4 +- .../channels/status/status_updates_spec.ts | 8 +- .../tests/support/common_login_commands.js | 2 +- .../cypress/tests/support/ui/global_header.js | 17 +- e2e-tests/cypress/tests/support/ui/menu.js | 12 +- .../src/actions/views/status_dropdown.test.ts | 28 - .../src/actions/views/status_dropdown.ts | 11 - .../custom_status/custom_status_emoji.tsx | 3 +- .../components/custom_status/expiry_time.tsx | 3 +- .../src/components/emoji/render_emoji.tsx | 3 +- .../right_controls/right_controls.tsx | 10 +- webapp/channels/src/components/menu/index.ts | 3 + webapp/channels/src/components/menu/menu.scss | 28 + webapp/channels/src/components/menu/menu.tsx | 50 +- .../src/components/menu/menu_item.tsx | 26 +- .../src/components/menu/menu_styled.tsx | 32 - .../src/components/menu/sub_menu.scss | 11 - .../channels/src/components/menu/sub_menu.tsx | 16 +- .../complete_your_profile_tour_tip.tsx | 3 +- .../onboarding_tasks_manager.tsx | 4 +- .../post_header_custom_status.tsx | 8 +- .../status_dropdown.test.tsx.snap | 2552 ----------------- .../src/components/status_dropdown/index.ts | 73 - .../status_dropdown/status_dropdown.scss | 358 --- .../status_dropdown/status_dropdown.test.tsx | 194 -- .../status_dropdown/status_dropdown.tsx | 701 ----- .../user_account_name_menuitem.test.tsx.snap | 32 + .../src/components/user_account_menu/index.ts | 61 + .../user_account_away_menuitem.tsx | 80 + .../user_account_custom_status_menuitem.tsx | 140 + .../user_account_dnd_menuitem.tsx | 305 ++ .../user_account_logout_menuitem.tsx | 35 + .../user_account_menu/user_account_menu.scss | 172 ++ .../user_account_menu/user_account_menu.tsx | 133 + .../user_account_menuButton.tsx | 139 + .../user_account_name_menuitem.test.tsx | 92 + .../user_account_name_menuitem.tsx | 78 + .../user_account_offline_menuitem.tsx | 80 + .../user_account_online_menuitem.tsx | 80 + ...er_account_out_of_office_menuitem.test.tsx | 57 + .../user_account_out_of_office_menuitem.tsx | 76 + .../user_account_profile_menuitem.tsx | 80 + ...ser_account_set_custom_status_menuitem.tsx | 35 + .../view_user_group_list_item.tsx | 1 - webapp/channels/src/i18n/en.json | 57 +- webapp/channels/src/reducers/views/index.ts | 2 - .../src/reducers/views/status_dropdown.ts | 26 - .../channels/src/sass/utils/_variables.scss | 3 + .../selectors/views/status_dropdown.test.ts | 19 - .../src/selectors/views/status_dropdown.ts | 8 - webapp/channels/src/types/store/views.ts | 4 - webapp/channels/src/utils/constants.tsx | 1 - webapp/channels/src/utils/keyboard.ts | 17 + .../src/pulsating_dot/pulsating_dot.scss | 34 + 75 files changed, 2020 insertions(+), 4305 deletions(-) delete mode 100644 webapp/channels/src/actions/views/status_dropdown.test.ts delete mode 100644 webapp/channels/src/actions/views/status_dropdown.ts create mode 100644 webapp/channels/src/components/menu/menu.scss delete mode 100644 webapp/channels/src/components/menu/menu_styled.tsx delete mode 100644 webapp/channels/src/components/menu/sub_menu.scss delete mode 100644 webapp/channels/src/components/status_dropdown/__snapshots__/status_dropdown.test.tsx.snap delete mode 100644 webapp/channels/src/components/status_dropdown/index.ts delete mode 100644 webapp/channels/src/components/status_dropdown/status_dropdown.scss delete mode 100644 webapp/channels/src/components/status_dropdown/status_dropdown.test.tsx delete mode 100644 webapp/channels/src/components/status_dropdown/status_dropdown.tsx create mode 100644 webapp/channels/src/components/user_account_menu/__snapshots__/user_account_name_menuitem.test.tsx.snap create mode 100644 webapp/channels/src/components/user_account_menu/index.ts create mode 100644 webapp/channels/src/components/user_account_menu/user_account_away_menuitem.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_custom_status_menuitem.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_dnd_menuitem.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_logout_menuitem.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_menu.scss create mode 100644 webapp/channels/src/components/user_account_menu/user_account_menu.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_menuButton.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_name_menuitem.test.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_name_menuitem.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_offline_menuitem.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_online_menuitem.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_out_of_office_menuitem.test.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_out_of_office_menuitem.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_profile_menuitem.tsx create mode 100644 webapp/channels/src/components/user_account_menu/user_account_set_custom_status_menuitem.tsx delete mode 100644 webapp/channels/src/reducers/views/status_dropdown.ts delete mode 100644 webapp/channels/src/selectors/views/status_dropdown.test.ts delete mode 100644 webapp/channels/src/selectors/views/status_dropdown.ts diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_image_spec.js b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_image_spec.js index c5963ef7e1..5703f9c38a 100644 --- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_image_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_image_spec.js @@ -26,9 +26,9 @@ describe('Verify Accessibility Support in Different Images', () => { it('MM-T1508 Accessibility support in different images', () => { // * Verify image alt in profile image - cy.uiGetProfileHeader(). - find('.Avatar'). - should('have.attr', 'alt', 'user profile image'); + cy.get('#userAccountMenuButton').within(() => { + cy.findByAltText('user profile image').should('be.visible'); + }); // # Upload an image in the post cy.get('#fileUploadInput').attachFile('small-image.png'); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/email_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/email_spec.ts index 44bac52cc8..8a264d2579 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/email_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/email_spec.ts @@ -163,9 +163,6 @@ describe('Profile > Profile Settings > Email', () => { // # Click on the link cy.visit(permalink); - // * Verify announcement bar - cy.get('.announcement-bar').should('be.visible').should('contain.text', 'Email verified'); - // # Wait for one second for the mail to be sent out. cy.wait(TIMEOUTS.FIVE_SEC); @@ -199,7 +196,7 @@ describe('Profile > Profile Settings > Email', () => { // # Close modal then logout cy.get('body').type('{esc}'); - cy.uiOpenUserMenu('Log Out'); + cy.uiLogout(); // # Wait for one second for the mail to be sent out. cy.wait(TIMEOUTS.ONE_SEC); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/username_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/username_spec.ts index 7fe8a4d767..6e2c5a7c6b 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/username_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/username_spec.ts @@ -116,8 +116,8 @@ describe('Settings > Sidebar > General > Edit', () => { // # Step 3 // * Verify that we've logged in as the temp user cy.visit(offTopicUrl); - cy.uiOpenUserMenu().findByText(`@${newTempUserName}`); - cy.uiGetSetStatusButton().click(); + cy.uiOpenUserMenu().findByText(`@${newTempUserName}`).should('exist'); + cy.get('body').type('{esc}'); // # Step 4 const text = `${newTempUserName} test message!`; diff --git a/e2e-tests/cypress/tests/integration/channels/archived_channel/join_archived_channel_spec.ts b/e2e-tests/cypress/tests/integration/channels/archived_channel/join_archived_channel_spec.ts index 231d8579ff..e4dd367d1a 100644 --- a/e2e-tests/cypress/tests/integration/channels/archived_channel/join_archived_channel_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/archived_channel/join_archived_channel_spec.ts @@ -124,5 +124,5 @@ function verifyUsername(username) { cy.uiOpenUserMenu().findByText(`@${username}`); // # Close the user menu - cy.uiGetSetStatusButton().click(); + cy.get('body').type('{esc}'); } diff --git a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/sidebar_category_menu_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/sidebar_category_menu_spec.ts index 16f3e9cf1a..5413cae2de 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/sidebar_category_menu_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/sidebar_category_menu_spec.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import * as TIMEOUTS from '../../../fixtures/timeouts'; + import {clickCategoryMenuItem} from './helpers'; // *************************************************************** @@ -22,6 +24,8 @@ describe('Sidebar category menu', () => { it('MM-T3171_1 Verify that the 3-dot menu on the Channels Category contains an option to Create New Category', () => { clickCategoryMenuItem('CHANNELS', 'Create New Category'); + cy.wait(TIMEOUTS.ONE_SEC); + cy.get('body').type('{esc}', {force: true}); }); @@ -41,6 +45,8 @@ describe('Sidebar category menu', () => { // # Verify that Create New Category exists on Favorites category and click on it clickCategoryMenuItem('FAVORITES', 'Create New Category'); + cy.wait(TIMEOUTS.ONE_SEC); + cy.get('body').type('{esc}', {force: true}); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_2_spec.ts b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_2_spec.ts index 8fc58cc757..16f453e945 100644 --- a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_2_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_2_spec.ts @@ -28,15 +28,23 @@ describe('Custom Status - Setting a Custom Status', () => { it('MM-T3836_1 should open status dropdown', () => { // # Click on the sidebar header to open status dropdown - cy.get('.MenuWrapper .status-wrapper').click(); + cy.uiGetSetStatusButton().click(); // * Check if the status dropdown opens - cy.get('#statusDropdownMenu').should('exist'); + cy.uiGetStatusMenuContainer(); + + // # Close the status dropdown + cy.get('body').type('{esc}'); }); it('MM-T3836_2 Custom status modal opens with 5 default statuses listed', () => { + // # Click on the sidebar header to open status dropdown + cy.uiGetSetStatusButton().click(); + + // * Check if the status dropdown opens + cy.uiGetStatusMenuContainer().findByText('Set custom status').click(); + // # Open custom status modal - cy.get('#statusDropdownMenu li#status-menu-custom-status').click(); cy.get('#custom_status_modal').should('exist'); // * Check if all the default suggestions exist @@ -111,13 +119,15 @@ describe('Custom Status - Setting a Custom Status', () => { trigger('mouseover'); // * Custom status tooltip should be visible - cy.get('#custom-status-tooltip').should('exist'); + cy.get('#custom-status-tooltip').should('exist').and('be.visible'); - // * Tooltip should contain the correct custom status emoji - cy.get('#custom-status-tooltip .custom-status span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji); + cy.get('#custom-status-tooltip').within(() => { + // * Tooltip should contain the correct custom status emoji + cy.get(`span[data-emoticon="${customStatus.emoji}"]`).should('exist'); - // * Tooltip should contain the correct custom status text - cy.get('#custom-status-tooltip .custom-status span.custom-status-text').should('have.text', customStatus.text); + // * Tooltip should contain the correct custom status text + cy.findByText(customStatus.text).should('exist'); + }); }); it('MM-T3836_8 should open custom status modal when emoji in LHS header is clicked', () => { diff --git a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_3_spec.ts b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_3_spec.ts index b9a69f6671..ad1591b439 100644 --- a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_3_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_3_spec.ts @@ -27,7 +27,7 @@ describe('Custom Status - Setting Your Own Custom Status', () => { it('MM-T3846_1 should change the emoji to speech balloon when typed in the input', () => { // # Open the custom status modal - cy.uiOpenUserMenu('Set a custom status'); + cy.uiOpenUserMenu('Set custom status'); // * Default emoji is currently visible in the custom status input cy.get('#custom_status_modal .StatusModal__emoji-button span').should('have.class', 'icon--emoji'); @@ -72,20 +72,26 @@ describe('Custom Status - Setting Your Own Custom Status', () => { cy.uiGetProfileHeader(). find('.emoticon'). should('have.attr', 'data-emoticon', customStatus.emoji); + + cy.get('body').type('{esc}'); }); it('MM-T3846_5 should show custom status with emoji in the status dropdown', () => { // # Open user menu - cy.uiOpenUserMenu(); + cy.uiOpenUserMenu().within(() => { + // * Correct custom status text and emoji should be displayed in the status dropdown + cy.findByText(customStatus.text).should('exist'); + cy.get(`span[data-emoticon="${customStatus.emoji}"]`).should('exist'); + }); - // * Correct custom status text and emoji should be displayed in the status dropdown - cy.get('.status-dropdown-menu .custom_status__container').should('have.text', customStatus.text); - cy.get('.status-dropdown-menu .custom_status__row span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji); + cy.get('body').type('{esc}'); }); - it('MM-T3846_6 should show previosly set status in the first position in Recents list', () => { + it('MM-T3846_6 should show previously set status in the first position in Recents list', () => { // # Click on the "Set a Custom Status" option in the status dropdown - cy.get('.status-dropdown-menu li#status-menu-custom-status').click(); + cy.uiOpenUserMenu().within(() => { + cy.findByText(customStatus.text).should('exist').click({force: true}); + }); // * Custom status modal should open cy.get('#custom_status_modal').should('exist'); @@ -118,9 +124,9 @@ describe('Custom Status - Setting Your Own Custom Status', () => { cy.findByText('Clear Status').click(); // # Open user menu - cy.uiOpenUserMenu(); - - // * Custom status text should not be displayed in the status dropdown - cy.get('.status-dropdown-menu .custom_status__row').should('not.have.text', customStatus.text); + cy.uiOpenUserMenu().within(() => { + // * Custom status text should not be displayed in the status dropdown + cy.findByText(customStatus.text).should('not.exist'); + }); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_4_spec.ts b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_4_spec.ts index 8daaf69319..8caddd651e 100644 --- a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_4_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_4_spec.ts @@ -32,7 +32,7 @@ describe('Custom Status - Recent Statuses', () => { it('MM-T3847_1 set a status', () => { // # Open the custom status modal - cy.uiOpenUserMenu('Set a custom status'); + cy.uiOpenUserMenu('Set custom status'); // # Type the custom status text in the custom status modal input cy.get('#custom_status_modal .StatusModal__input input').typeWithForce(customStatus.text); diff --git a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_5_spec.ts b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_5_spec.ts index 1cc20c8d76..764e810180 100644 --- a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_5_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_5_spec.ts @@ -27,7 +27,7 @@ describe('Custom Status - Verifying Where Custom Status Appears', () => { }); it('MM-T3850_1 set a status', () => { - cy.uiOpenUserMenu('Set a Custom Status'); + cy.uiOpenUserMenu('Set custom status'); // # Type the custom status text in the custom status modal input cy.findByPlaceholderText('Set a status').type(customStatus.text, {force: true}); diff --git a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_1_spec.ts b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_1_spec.ts index 13ae7afbb4..d09034c61b 100644 --- a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_1_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_1_spec.ts @@ -25,6 +25,7 @@ describe('MM-T4063 Custom status expiry', () => { const defaultCustomStatuses = ['In a meeting', 'Out for lunch', 'Out sick', 'Working from home', 'On a vacation']; const customStatus = { emoji: 'hamburger', + emojiAriaLabel: ':hamburger:', text: 'Out for lunch', duration: '30 minutes', }; @@ -36,19 +37,25 @@ describe('MM-T4063 Custom status expiry', () => { const expiryTimeFormat = 'h:mm A'; it('MM-T4063_1 should open status dropdown', () => { // # Click on the sidebar header to open status dropdown - cy.get('.MenuWrapper .status-wrapper').click(); + cy.uiGetSetStatusButton().click(); // * Check if the status dropdown opens - cy.get('#statusDropdownMenu').should('exist'); + cy.uiGetStatusMenu(); + + // # Close the status dropdown + cy.get('body').click(); }); it('MM-T4063_2 Custom status modal opens with 5 default statuses listed', () => { // # Open custom status modal - cy.get('#statusDropdownMenu li#status-menu-custom-status').click(); - cy.get('#custom_status_modal').should('exist'); + cy.uiOpenUserMenu('Set custom status'); - // * Check if all the default suggestions exist - defaultCustomStatuses.map((statusText) => cy.get('#custom_status_modal .statusSuggestion__content').contains('span', statusText)); + cy.findByRole('dialog', {name: 'Set a status'}).should('exist').within(() => { + // * Check if all the default suggestions exist + defaultCustomStatuses.forEach((statusText) => { + cy.findByText(statusText).should('exist'); + }); + }); }); it('MM-T4063_3 Correct custom status is selected with the correct emoji and correct duration', () => { @@ -94,17 +101,17 @@ describe('MM-T4063 Custom status expiry', () => { it('MM-T4063_5 should show the set custom status with expiry when status dropdown is opened', () => { // # Click on the sidebar header to open status dropdown - cy.get('.MenuWrapper .status-wrapper').click(); + cy.uiGetSetStatusButton().click(); // * Check if the status dropdown opens - cy.get('#statusDropdownMenu').should('exist'); + cy.uiGetStatusMenu().within(() => { + // * Correct custom status text and emoji should be displayed in the status dropdown + cy.findByText(customStatus.text).should('exist'); + cy.findByLabelText(customStatus.emojiAriaLabel).should('exist'); - // * Correct custom status text and emoji should be displayed in the status dropdown - cy.get('.status-dropdown-menu .custom_status__container').should('have.text', customStatus.text); - cy.get('.status-dropdown-menu .custom_status__row span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji); - - // * Correct clear time should be displayed in the status dropdown - cy.get('.status-dropdown-menu .custom_status__expiry time').invoke('text').should('match', expiresAtRegexp); + // * Correct clear time should be displayed in the status dropdown + cy.findByText(expiresAtRegexp).should('exist'); + }); }); it('MM-T4063_6 custom status should be cleared after duration of set custom status', () => { @@ -118,8 +125,7 @@ describe('MM-T4063 Custom status expiry', () => { it('MM-T4063_7 current custom status should display expiry time in custom status modal', () => { // # Open custom status modal - cy.get('#statusDropdownMenu li#status-menu-custom-status').click(); - cy.get('#custom_status_modal').should('exist'); + cy.get('.userAccountMenu_customStatusMenuItem').should('be.visible').click(); // * Should show expiry time of status when current status is selected cy.get('#custom_status_modal .statusSuggestion__content').contains('span', customStatus.text).click(); @@ -132,15 +138,12 @@ describe('MM-T4063 Custom status expiry', () => { it('MM-T4063_8 previous custom status duration should be reset if custom status is expired', () => { // # Forwarding the time by the duration of custom status cy.clock(Date.now()); - cy.tick(waitingTime * 60 * 1000); - - // # Open custom status modal - cy.get('.status-dropdown-menu').click(); - cy.get('#statusDropdownMenu li#status-menu-custom-status').click(); - cy.get('#custom_status_modal').should('exist'); - - // * Should show duration of previous status when previous status is selected - cy.get('#custom_status_modal .statusSuggestion__content').contains('span', customStatus.text).click(); - cy.get('#custom_status_modal .expiry-value').should('have.text', customStatus.duration); + cy.tick(waitingTime * 60 * 1000).then(() => { + // # Open custom status modal + cy.uiOpenUserMenu().within(() => { + // * Verify that there is no custom status in the dropdown + cy.findByText('Set custom status').should('exist').and('be.visible'); + }); + }); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_2_spec.ts b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_2_spec.ts index a15928b29c..61474e4ae8 100644 --- a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_2_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_2_spec.ts @@ -21,86 +21,30 @@ describe('MM-T4064 Status expiry visibility', () => { cy.visit(channelUrl); }); }); - - const defaultCustomStatuses = ['In a meeting', 'Out for lunch', 'Out sick', 'Working from home', 'On a vacation']; - const customStatus = { - emoji: 'calendar', - text: 'In a meeting', - duration: '1 hour', - }; - const waitingTime = 60; //minutes let expiresAt = dayjs(); const expiryTimeFormat = 'h:mm A'; - it('MM-T4064_1 should open status dropdown', () => { - // # Click on the sidebar header to open status dropdown - cy.get('.MenuWrapper .status-wrapper').click(); - // * Check if the status dropdown opens - cy.get('#statusDropdownMenu').should('exist'); - }); + it('MM-T4064_6 should show expiry time in the tooltip of custom status emoji in the post header', () => { + // # Open the user account menu + cy.uiOpenUserMenu('Set custom status'); - it('MM-T4064_2 Custom status modal opens with 5 default statuses listed', () => { - // # Open custom status modal - cy.get('#statusDropdownMenu li#status-menu-custom-status').click(); - cy.get('#custom_status_modal').should('exist'); + // * Verify that the custom status modal opens + cy.findByRole('dialog', {name: 'Set a status'}).should('exist').within(() => { + // # Select a custom status from the suggestions + cy.get('.statusSuggestion__row').first().click(); - // * Check if all the default suggestions exist - defaultCustomStatuses.map((statusText) => cy.get('#custom_status_modal .statusSuggestion__content').contains('span', statusText)); - }); - - it('MM-T4064_3 Correct custom status is selected with the correct emoji and correct duration', () => { - // * Default emoji is currently visible in the custom status input - cy.get('#custom_status_modal .StatusModal__emoji-button span').should('have.class', 'icon--emoji'); - - // * Input should be empty - cy.get('#custom_status_modal input.form-control').should('have.value', ''); - - // # Select a custom status from the suggestions - cy.get('#custom_status_modal .statusSuggestion__content').contains('span', customStatus.text).click(); - - // * Emoji in the custom status input should be changed - cy.get('#custom_status_modal .StatusModal__emoji-button span').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji); - - // * Selected custom status text should be in the input - cy.get('#custom_status_modal input.form-control').should('have.value', customStatus.text); - - // * Selected custom status duration should be displayed in the Clear after section - cy.get('#custom_status_modal .expiry-wrapper .expiry-value').should('have.text', customStatus.duration); - }); - - it('MM-T4064_4 should set custom status when click on Set Status', () => { - // # Click on the Set Status button - cy.get('#custom_status_modal .GenericModal__button.confirm').click(); + // # Click on the Set Status button + cy.findByText('Set Status').click(); + }); // * Modal should be closed cy.get('#custom_status_modal').should('not.exist'); // # Setting the time at which the custom status should be expired + // # Note that we need to be flexible around accepted values, as this calculation and the server-side one may differ slightly expiresAt = dayjs().add(waitingTime, 'minute'); - // * Status should be set and the emoji should be visible in the sidebar header - cy.uiGetProfileHeader(). - find('.emoticon'). - should('have.attr', 'data-emoticon', customStatus.emoji); - }); - - it('MM-T4064_5 should show the set custom status with expiry when status dropdown is opened', () => { - // # Click on the sidebar header to open status dropdown - cy.get('.MenuWrapper .status-wrapper').click(); - - // * Check if the status dropdown opens - cy.get('#statusDropdownMenu').should('exist'); - - // * Correct custom status text and emoji should be displayed in the status dropdown - cy.get('.status-dropdown-menu .custom_status__container').should('have.text', customStatus.text); - cy.get('.status-dropdown-menu .custom_status__row span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji); - - // * Correct clear time should be displayed in the status dropdown - cy.get('.status-dropdown-menu .custom_status__expiry time').should('have.text', expiresAt.format(expiryTimeFormat)); - }); - - it('MM-T4064_6 should show expiry time in the tooltip of custom status emoji in the post header', () => { // # Post a message in the channel cy.postMessage('Hello World!'); diff --git a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_3_spec.ts b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_3_spec.ts index 67eaebd3ae..ede43e3f20 100644 --- a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_3_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_3_spec.ts @@ -39,15 +39,15 @@ describe('MM-T4065 Setting manual status clear time less than 7 days away', () = it('MM-T4065_1 should open status dropdown', () => { // # Click on the sidebar header to open status dropdown - cy.get('.MenuWrapper .status-wrapper').click(); + cy.uiGetSetStatusButton().click(); // * Check if the status dropdown opens - cy.get('#statusDropdownMenu').should('exist'); + cy.get('#userAccountMenu').should('exist'); }); it('MM-T4065_2 Custom status modal opens with 5 default statuses listed', () => { // # Open custom status modal - cy.get('#statusDropdownMenu li#status-menu-custom-status').click(); + cy.get('.userAccountMenu_setCustomStatusMenuItem').click(); cy.get('#custom_status_modal').should('exist'); // * Check if all the default suggestions exist diff --git a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_4_spec.ts b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_4_spec.ts index d75de18c17..7307dc3709 100644 --- a/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_4_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/custom_status/custom_status_expiry/custom_status_expiry_4_spec.ts @@ -38,15 +38,15 @@ describe('MM-T4066 Setting manual status clear time more than 7 days away', () = const months = dateToBeSelected.get('month') - today.get('month'); it('MM-T4066_1 should open status dropdown', () => { // # Click on the sidebar header to open status dropdown - cy.get('.MenuWrapper .status-wrapper').click(); + cy.uiGetSetStatusButton().click(); // * Check if the status dropdown opens - cy.get('#statusDropdownMenu').should('exist'); + cy.get('#userAccountMenu').should('exist'); }); it('MM-T4066_2 Custom status modal opens with 5 default statuses listed', () => { // # Open custom status modal - cy.get('#statusDropdownMenu li#status-menu-custom-status').click(); + cy.get('.userAccountMenu_setCustomStatusMenuItem').click(); cy.get('#custom_status_modal').should('exist'); // * Check if all the default suggestions exist diff --git a/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/user_status_commands_spec.js b/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/user_status_commands_spec.js index d44747c6d0..840be19f45 100644 --- a/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/user_status_commands_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/user_status_commands_spec.js @@ -12,10 +12,10 @@ describe('Integrations', () => { const testCases = [ - {command: '/away', className: 'icon-clock', message: 'You are now away'}, - {command: '/dnd', className: 'icon-minus-circle', message: 'Do Not Disturb is enabled. You will not receive desktop or mobile push notifications until Do Not Disturb is turned off.'}, - {command: '/offline', className: 'icon-circle-outline', message: 'You are now offline'}, - {command: '/online', className: 'icon-check-circle', message: 'You are now online'}, + {command: '/away', className: 'userAccountMenu_awayMenuItem_icon', message: 'You are now away'}, + {command: '/dnd', className: 'userAccountMenu_dndMenuItem_icon', message: 'Do Not Disturb is enabled. You will not receive desktop or mobile push notifications until Do Not Disturb is turned off.'}, + {command: '/offline', className: 'userAccountMenu_offlineMenuItem_icon', message: 'You are now offline'}, + {command: '/online', className: 'userAccountMenu_onlineMenuItem_icon', message: 'You are now online'}, ]; let offTopicUrl; @@ -67,7 +67,7 @@ describe('Integrations', () => { function verifyUserStatus(testCase, isCompactMode) { // * Verify that the user status is as indicated cy.uiGetProfileHeader(). - find('i'). + find('svg'). should('be.visible'). and('have.class', testCase.className); diff --git a/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/user_status_spec.js b/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/user_status_spec.js index aadb723205..c5487cfdb6 100644 --- a/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/user_status_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/user_status_spec.js @@ -26,14 +26,14 @@ describe('Integrations', () => { it('MM-T670 /away', () => { // # Set online status and verify it's changed as the initial status - setStatus(online.name, online.profileClassName); + setStatus(online.name); verifyUserStatus(away); }); it('MM-T672 /offline', () => { // # Set online status and verify it's changed as the initial status - setStatus(online.name, online.profileClassName); + setStatus(online.name); verifyUserStatus(offline); @@ -60,17 +60,14 @@ describe('Integrations', () => { it('MM-T674 /online', () => { // # Set offline status and verify it's changed as the initial status - setStatus(offline.name, offline.className); + setStatus(offline.name); verifyUserStatus(online); }); }); -function setStatus(status, icon) { +function setStatus(status) { cy.apiUpdateUserStatus(status); - cy.uiGetProfileHeader(). - find('i'). - and('have.class', icon); } function verifyUserStatus(testCase) { @@ -90,9 +87,7 @@ function verifyUserStatus(testCase) { }); // * Verify status shown at user profile in LHS - cy.uiGetProfileHeader(). - find('i'). - and('have.class', testCase.profileClassName || testCase.className); + cy.get('#userAccountMenuButtonDescribedBy').should('exist').and('include.text', `Status is "${testCase.name.charAt(0).toUpperCase() + testCase.name.slice(1)}"`); // # Post a message cy.postMessage(testCase.name); diff --git a/e2e-tests/cypress/tests/integration/channels/menus/main_menu_spec.js b/e2e-tests/cypress/tests/integration/channels/menus/main_menu_spec.js index 4cf116830a..ff5770ac3c 100644 --- a/e2e-tests/cypress/tests/integration/channels/menus/main_menu_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/menus/main_menu_spec.js @@ -33,10 +33,9 @@ describe('Main menu', () => { cy.apiLogin(testUser); cy.visit(`/${testTeam.name}/channels/town-square`); - cy.uiOpenUserMenu(); - - cy.findByRole('menu').should('exist').find('.menu-divider:visible').last().click(); - cy.findByRole('menu').should('exist'); + cy.uiOpenUserMenu().within(() => { + cy.findAllByRole('separator').last().click(); + }); }); it('should show integrations option for system administrator', () => { diff --git a/e2e-tests/cypress/tests/integration/channels/menus/status_dropdown_spec.js b/e2e-tests/cypress/tests/integration/channels/menus/status_dropdown_spec.js index 1d10ee66f0..2e5892b485 100644 --- a/e2e-tests/cypress/tests/integration/channels/menus/status_dropdown_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/menus/status_dropdown_spec.js @@ -10,15 +10,14 @@ // Stage: @prod // Group: @channels @menu @custom_status @status_menu -import * as TIMEOUTS from '../../../fixtures/timeouts'; import theme from '../../../fixtures/theme.json'; describe('Status dropdown menu', () => { const statusTestCases = [ - {text: 'Online', className: 'icon-check-circle', profileClassName: 'icon-check-circle'}, - {text: 'Away', className: 'icon-clock'}, - {text: 'Do not disturb', className: 'icon-minus-circle'}, - {text: 'Offline', className: 'icon-circle-outline'}, + {text: 'Online', className: 'userAccountMenu_onlineMenuItem_icon'}, + {text: 'Away', className: 'userAccountMenu_awayMenuItem_icon'}, + {text: 'Do not disturb', className: 'userAccountMenu_dndMenuItem_icon'}, + {text: 'Offline', className: 'userAccountMenu_offlineMenuItem_icon'}, ]; before(() => { @@ -40,9 +39,10 @@ describe('Status dropdown menu', () => { // * Verify all available statuses are shown with icon and text statusTestCases.forEach((tc) => { - cy.get('@userMenu').findByText(tc.text). - parent(). - find('i').should('have.class', tc.className); + cy.uiGetStatusMenu().within(() => { + cy.findByText(tc.text).should('be.visible'); + cy.get('svg').filter(`.${tc.className}`).should('have.length', 1); + }); }); }); @@ -67,7 +67,7 @@ describe('Status dropdown menu', () => { cy.uiOpenUserMenu().as('userMenu'); // * Verify "Set a Custom Status" header is clickable - cy.get('@userMenu').findByText('Set a custom status').should('have.css', 'cursor', 'pointer'); + cy.get('@userMenu').findByText('Set custom status').should('have.css', 'cursor', 'pointer'); }); it('MM-T2927_5 When custom status is disabled, status menu is displayed when status icon is clicked', () => { @@ -81,22 +81,12 @@ describe('Status dropdown menu', () => { cy.uiOpenUserMenu(); }); - it('MM-T4420 Should stay open when dnd sub-menu header is clicked', () => { - // # Open Dnd sub menu and click on header - cy.uiOpenDndStatusSubMenu().find('#dndSubMenu-header_menuitem').click().then(() => { - cy.wait(TIMEOUTS.HALF_SEC); - - // * Verify that dnd submenu is still visible - cy.get('body').find('#dndSubMenu-header_menuitem').should('be.visible'); - }); - }); - it('MM-T4914 Profile menu header is clickable, opens Profile settings', () => { // # Open user menu - cy.uiOpenUserMenu().as('userMenu'); - - // * Verify menu header is clickable - cy.get('@userMenu').get('.MenuHeader').should('have.css', 'cursor', 'pointer').click(); + cy.uiOpenUserMenu().within(() => { + // * Verify menu header is clickable + cy.get('li').first().should('have.css', 'cursor', 'pointer').click(); + }); // * Verify click on header opens Profile settings modal cy.findByRole('dialog', {name: 'Profile'}).should('be.visible'); @@ -111,14 +101,14 @@ function stepThroughStatuses(statusTestCases = []) { statusTestCases.forEach((tc) => { // # Open user menu and click option if (tc.text === 'Do not disturb') { - cy.uiOpenDndStatusSubMenu().find('#dndTime-thirty_minutes_menuitem').click(); + cy.uiOpenDndStatusSubMenuAndClick30Mins(); } else { cy.uiOpenUserMenu(tc.text); } // # Verify correct status icon is shown on user's profile picture - cy.uiGetProfileHeader(). - find('i'). - and('have.class', tc.profileClassName || tc.className); + cy.uiGetSetStatusButton().within(() => { + cy.get('svg').filter(`.${tc.className}`).should('have.length', 1); + }); }); } diff --git a/e2e-tests/cypress/tests/integration/channels/notifications/users_with_same_firstname_channel_mentions_spec.js b/e2e-tests/cypress/tests/integration/channels/notifications/users_with_same_firstname_channel_mentions_spec.js index 4a230d3ec0..6b1045d0e8 100644 --- a/e2e-tests/cypress/tests/integration/channels/notifications/users_with_same_firstname_channel_mentions_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/notifications/users_with_same_firstname_channel_mentions_spec.js @@ -78,7 +78,7 @@ describe('Notifications', () => { cy.uiOpenUserMenu().findByText(`@${firstUser.username}`); // # Close the user menu - cy.uiGetSetStatusButton().click(); + cy.get('body').type('{esc}'); // * Check that 'Town Square' is currently being selected cy.get('.active').within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/profile_settings/profile_settings_spec.js b/e2e-tests/cypress/tests/integration/channels/profile_settings/profile_settings_spec.js index b8dc510ef6..827dcb6ec8 100644 --- a/e2e-tests/cypress/tests/integration/channels/profile_settings/profile_settings_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/profile_settings/profile_settings_spec.js @@ -71,7 +71,7 @@ describe('Profile Settings', () => { fileTypes.forEach((fileType, index) => { it(`MM-T2078_${index + 1} Profile picture: file ${fileType.extension} type accepted`, () => { // # Save the default profile picture link so it can be compared to the new one - cy.uiGetProfileHeader().findByRole('img').invoke('attr', 'src').as('defaultProfilePictureLink'); + cy.uiGetSetStatusButton().get('img').invoke('attr', 'src').as('defaultProfilePictureLink'); cy.uiOpenProfileModal('Profile Settings'); @@ -95,7 +95,7 @@ describe('Profile Settings', () => { cy.uiClose(); // # Save the new custom profile picture link so it can be compared to the old one - cy.uiGetProfileHeader().findByRole('img').invoke('attr', 'src').as('customProfilePictureLink'); + cy.uiGetSetStatusButton().get('img').invoke('attr', 'src').as('customProfilePictureLink'); cy.then(function() { expect(this.customProfilePictureLink).to.not.equal(this.defaultProfilePictureLink); diff --git a/e2e-tests/cypress/tests/integration/channels/settings/display/theme/custom_theme_sidebar_styles_spec.js b/e2e-tests/cypress/tests/integration/channels/settings/display/theme/custom_theme_sidebar_styles_spec.js index 4bf17e1503..ce8c30f1ca 100644 --- a/e2e-tests/cypress/tests/integration/channels/settings/display/theme/custom_theme_sidebar_styles_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/settings/display/theme/custom_theme_sidebar_styles_spec.js @@ -92,18 +92,18 @@ describe('Custom Theme - Sidebar Styles', () => { cy.uiOpenUserMenu('Online'); // * Check Online Indicator color - cy.get('.icon-check-circle').should('have.css', 'color', rgbArrayToString(themeRgbColor.onlineIndicator)); + cy.uiGetSetStatusButton().find('svg').should('have.css', 'color', rgbArrayToString(themeRgbColor.onlineIndicator)); // # Set user status to away cy.uiOpenUserMenu('Away'); // * Check Away Indicator color - cy.get('.icon-clock').should('have.css', 'color', rgbArrayToString(themeRgbColor.awayIndicator)); + cy.uiGetSetStatusButton().find('svg').should('have.css', 'color', rgbArrayToString(themeRgbColor.awayIndicator)); // # Set user status to do not disturb - cy.uiOpenDndStatusSubMenu().find('#dndTime-thirty_minutes_menuitem').click(); + cy.uiOpenDndStatusSubMenuAndClick30Mins(); // * Check Do Not Disturb Indicator color - cy.get('.icon-minus-circle').should('have.css', 'color', rgbArrayToString(themeRgbColor.dndIndicator)); + cy.uiGetSetStatusButton().find('svg').should('have.css', 'color', rgbArrayToString(themeRgbColor.dndIndicator)); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/signin_authentication/login_logout_smoke_spec.js b/e2e-tests/cypress/tests/integration/channels/signin_authentication/login_logout_smoke_spec.js index 80ad91fa62..c104181236 100644 --- a/e2e-tests/cypress/tests/integration/channels/signin_authentication/login_logout_smoke_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/signin_authentication/login_logout_smoke_spec.js @@ -42,7 +42,7 @@ describe('SignIn Authentication', () => { cy.url().should('include', '/channels/town-square'); // # Click logout via user menu - cy.uiOpenUserMenu('Log Out'); + cy.uiOpenUserMenu('Log out'); // * Check that it logout successfully and it redirects into the login page cy.url().should('include', '/login'); diff --git a/e2e-tests/cypress/tests/integration/channels/signin_authentication/login_open_server_spec.js b/e2e-tests/cypress/tests/integration/channels/signin_authentication/login_open_server_spec.js index c4219a7947..bfb0f199d4 100644 --- a/e2e-tests/cypress/tests/integration/channels/signin_authentication/login_open_server_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/signin_authentication/login_open_server_spec.js @@ -186,7 +186,7 @@ describe('Login page with open server', () => { cy.url().should('include', '/channels/town-square'); // # Click logout via user menu - cy.uiOpenUserMenu('Log Out'); + cy.uiOpenUserMenu('Log out'); // * Check that it logout successfully and it redirects into the login page cy.url().should('include', '/login'); @@ -209,7 +209,7 @@ describe('Login page with open server', () => { cy.url().should('include', '/channels/town-square'); // # Click logout via user menu - cy.uiOpenUserMenu('Log Out'); + cy.uiOpenUserMenu('Log out'); // * Check that it logout successfully and it redirects into the login page cy.url().should('include', '/login'); diff --git a/e2e-tests/cypress/tests/integration/channels/status/status_updates_spec.ts b/e2e-tests/cypress/tests/integration/channels/status/status_updates_spec.ts index 78b904ce6b..bbe6bbdc18 100644 --- a/e2e-tests/cypress/tests/integration/channels/status/status_updates_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/status/status_updates_spec.ts @@ -92,12 +92,12 @@ describe('Status of current user', () => { }); function verifyStatus(status: 'online' | 'away' | 'offline' | 'dnd') { - cy.get('[aria-label="Current status: Online. Select to open profile and status menu."]'). + cy.get('[aria-label="Status is \\"Online\\". Open user\'s account menu."]'). should(status === 'online' ? 'exist' : 'not.exist'); - cy.get('[aria-label="Current status: Away. Select to open profile and status menu."]'). + cy.get('[aria-label="Status is \\"Away\\". Open user\'s account menu."]'). should(status === 'away' ? 'exist' : 'not.exist'); - cy.get('[aria-label="Current status: Offline. Select to open profile and status menu."]'). + cy.get('[aria-label="Status is \\"Offline\\". Open user\'s account menu."]'). should(status === 'offline' ? 'exist' : 'not.exist'); - cy.get('[aria-label="Current status: Do not disturb. Select to open profile and status menu."]'). + cy.get('[aria-label="Status is \\"Do not disturb\\". Open user\'s account menu."]'). should(status === 'dnd' ? 'exist' : 'not.exist'); } diff --git a/e2e-tests/cypress/tests/support/common_login_commands.js b/e2e-tests/cypress/tests/support/common_login_commands.js index 8cf8efc7dd..f732258575 100644 --- a/e2e-tests/cypress/tests/support/common_login_commands.js +++ b/e2e-tests/cypress/tests/support/common_login_commands.js @@ -53,7 +53,7 @@ Cypress.Commands.add('checkLeftSideBar', (settings = {}) => { cy.uiOpenUserMenu().findByText(`@${settings.user.username}`); // # Close status menu - cy.uiGetSetStatusButton().click(); + cy.get('body').type('{esc}'); } if (settings.user.userType === 'Admin' || settings.user.isAdmin) { diff --git a/e2e-tests/cypress/tests/support/ui/global_header.js b/e2e-tests/cypress/tests/support/ui/global_header.js index 64dc75ed45..4800e2841e 100644 --- a/e2e-tests/cypress/tests/support/ui/global_header.js +++ b/e2e-tests/cypress/tests/support/ui/global_header.js @@ -27,7 +27,8 @@ Cypress.Commands.add('uiOpenProductMenu', (item = '') => { }); Cypress.Commands.add('uiGetSetStatusButton', () => { - return cy.findByRole('button', {name: /Select to open profile and status menu\./i}).should('be.visible'); + // # Get set status button + return cy.get('#userAccountMenuButton').should('be.visible'); }); Cypress.Commands.add('uiGetProfileHeader', () => { @@ -36,22 +37,18 @@ Cypress.Commands.add('uiGetProfileHeader', () => { Cypress.Commands.add('uiGetStatusMenuContainer', (options = {exist: true}) => { if (options.exist) { - return cy.findByRole('menu').should('exist'); + return cy.get('#userAccountMenu').should('exist').and('be.visible'); } - return cy.findByRole('menu').should('not.exist'); + return cy.get('#userAccountMenu').should('not.exist'); }); Cypress.Commands.add('uiGetStatusMenu', (options = {visible: true}) => { if (options.visible) { - return cy.uiGetStatusMenuContainer(). - find('ul'). - should('be.visible'); + return cy.get('#userAccountMenu').should('exist').and('be.visible'); } - return cy.uiGetStatusMenuContainer(). - find('ul'). - should('not.be.visible'); + return cy.get('#userAccountMenu').should('not.exist'); }); Cypress.Commands.add('uiOpenHelpMenu', (item = '') => { @@ -147,7 +144,7 @@ Cypress.Commands.add('uiOpenSettingsModal', (section = '') => { Cypress.Commands.add('uiLogout', () => { // # Click logout via user menu - cy.uiOpenUserMenu('Log Out'); + cy.uiOpenUserMenu('Log out'); cy.url().should('include', '/login'); cy.get('.login-body-message').should('be.visible'); diff --git a/e2e-tests/cypress/tests/support/ui/menu.js b/e2e-tests/cypress/tests/support/ui/menu.js index 689f0e2816..6dbed1dfe2 100644 --- a/e2e-tests/cypress/tests/support/ui/menu.js +++ b/e2e-tests/cypress/tests/support/ui/menu.js @@ -33,14 +33,8 @@ Cypress.Commands.add('uiGetSystemConsoleMainMenu', () => { return getMenu(SYSTEM_CONSOLE_MAIN_MENU); }); -Cypress.Commands.add('uiOpenDndStatusSubMenu', () => { - cy.uiOpenUserMenu(); +Cypress.Commands.add('uiOpenDndStatusSubMenuAndClick30Mins', () => { + cy.uiOpenUserMenu().findByText('Do not disturb').should('be.visible').trigger('mouseover'); - // # Wait for status menu to transition in - cy.get('.MenuWrapper.status-dropdown-menu .Menu__content.dropdown-menu').should('be.visible'); - - // # Hover over Do Not Disturb option - cy.get('.MenuWrapper.status-dropdown-menu .Menu__content.dropdown-menu li#status-menu-dnd_menuitem').trigger('mouseover'); - - return cy.get('#status-menu-dnd'); + cy.findByText('30 mins').should('be.visible').click({force: true}); }); diff --git a/webapp/channels/src/actions/views/status_dropdown.test.ts b/webapp/channels/src/actions/views/status_dropdown.test.ts deleted file mode 100644 index 0b0da7a309..0000000000 --- a/webapp/channels/src/actions/views/status_dropdown.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {setStatusDropdown} from 'actions/views/status_dropdown'; -import {isStatusDropdownOpen} from 'selectors/views/status_dropdown'; -import configureStore from 'store'; - -describe('status_dropdown view actions', () => { - const initialState = { - views: { - statusDropdown: { - isOpen: false, - }, - }, - }; - - it('setStatusDropdown should set the status dropdown open or not', () => { - const store = configureStore(initialState); - - store.dispatch(setStatusDropdown(false)); - - expect(isStatusDropdownOpen(store.getState())).toBe(false); - - store.dispatch(setStatusDropdown(true)); - - expect(isStatusDropdownOpen(store.getState())).toBe(true); - }); -}); diff --git a/webapp/channels/src/actions/views/status_dropdown.ts b/webapp/channels/src/actions/views/status_dropdown.ts deleted file mode 100644 index 93fe4493cd..0000000000 --- a/webapp/channels/src/actions/views/status_dropdown.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {ActionTypes} from 'utils/constants'; - -export function setStatusDropdown(open: boolean) { - return { - type: ActionTypes.STATUS_DROPDOWN_TOGGLE, - open, - }; -} diff --git a/webapp/channels/src/components/custom_status/custom_status_emoji.tsx b/webapp/channels/src/components/custom_status/custom_status_emoji.tsx index daa932298c..5cee41e1e4 100644 --- a/webapp/channels/src/components/custom_status/custom_status_emoji.tsx +++ b/webapp/channels/src/components/custom_status/custom_status_emoji.tsx @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import React, {memo, useMemo, useRef} from 'react'; +import type {MouseEvent, KeyboardEvent} from 'react'; import {useSelector} from 'react-redux'; import {CustomStatusDuration} from '@mattermost/types/users'; @@ -23,7 +24,7 @@ interface Props { spanStyle?: React.CSSProperties; emojiStyle?: React.CSSProperties; userID?: string; - onClick?: () => void; + onClick?: (event: MouseEvent | KeyboardEvent) => void; } function CustomStatusEmoji({ diff --git a/webapp/channels/src/components/custom_status/expiry_time.tsx b/webapp/channels/src/components/custom_status/expiry_time.tsx index f598b6b85d..283461635c 100644 --- a/webapp/channels/src/components/custom_status/expiry_time.tsx +++ b/webapp/channels/src/components/custom_status/expiry_time.tsx @@ -57,7 +57,8 @@ const ExpiryTime = ({time, timezone, className, showPrefix, withinBrackets}: Pro id='custom_status.expiry.until' defaultMessage='Until {time}' values={{time: ''}} - />{' '} + /> + {' '} ); diff --git a/webapp/channels/src/components/emoji/render_emoji.tsx b/webapp/channels/src/components/emoji/render_emoji.tsx index c9558c2d09..4ed904a032 100644 --- a/webapp/channels/src/components/emoji/render_emoji.tsx +++ b/webapp/channels/src/components/emoji/render_emoji.tsx @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; +import type {MouseEvent, KeyboardEvent} from 'react'; import {useSelector} from 'react-redux'; import {getEmojiImageUrl} from 'mattermost-redux/utils/emoji_utils'; @@ -14,7 +15,7 @@ interface ComponentProps { emojiName: string; size?: number; emojiStyle?: React.CSSProperties; - onClick?: () => void; + onClick?: (event: MouseEvent | KeyboardEvent) => void; } const RenderEmoji = ({emojiName, emojiStyle, size, onClick}: ComponentProps) => { diff --git a/webapp/channels/src/components/global_header/right_controls/right_controls.tsx b/webapp/channels/src/components/global_header/right_controls/right_controls.tsx index defe1f7a5a..e06c09c0f7 100644 --- a/webapp/channels/src/components/global_header/right_controls/right_controls.tsx +++ b/webapp/channels/src/components/global_header/right_controls/right_controls.tsx @@ -9,12 +9,12 @@ import type {ProductIdentifier} from '@mattermost/types/products'; import {isCurrentUserGuestUser} from 'mattermost-redux/selectors/entities/users'; -import StatusDropdown from 'components/status_dropdown'; import {OnboardingTourSteps, OnboardingTourStepsForGuestUsers} from 'components/tours'; import { CustomizeYourExperienceTour, useShowOnboardingTutorialStep, } from 'components/tours/onboarding_tour'; +import UserAccountMenu from 'components/user_account_menu'; import Pluggable from 'plugins/pluggable'; import {isChannels} from 'utils/products'; @@ -46,10 +46,6 @@ const StyledCustomizeYourExperienceTour = styled.div` height: 100% `; -const StyledStatusDropdown = styled.div` - margin-left: 8px; -`; - export type Props = { productId?: ProductIdentifier; } @@ -87,9 +83,7 @@ const RightControls = ({productId = null}: Props): JSX.Element => { ) : null } - - - + ); diff --git a/webapp/channels/src/components/menu/index.ts b/webapp/channels/src/components/menu/index.ts index 66bbd91468..0f21a10199 100644 --- a/webapp/channels/src/components/menu/index.ts +++ b/webapp/channels/src/components/menu/index.ts @@ -1,7 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import './menu.scss'; + export {Menu as Container} from './menu'; export {SubMenu} from './sub_menu'; export {MenuItem as Item} from './menu_item'; +export type {FirstMenuItemProps} from './menu_item'; export {MenuItemSeparator as Separator} from './menu_item_separator'; diff --git a/webapp/channels/src/components/menu/menu.scss b/webapp/channels/src/components/menu/menu.scss new file mode 100644 index 0000000000..f8934f0236 --- /dev/null +++ b/webapp/channels/src/components/menu/menu.scss @@ -0,0 +1,28 @@ +@use "utils/variables"; + +.MuiPopover-root.MuiModal-root.menu_menuStyled { + z-index: variables.$z-index-menu; + + & .MuiPaper-root { + min-width: 114px; + max-width: 496px; + max-height: 80vh; + background-color: var(--center-channel-bg); + box-shadow: var(--elevation-4); + } + &.AsSubMenu { + & .MuiPaper-root { + box-shadow: var(--elevation-5); + } + } +} + +.menuModal { + & .modal-header { + display: none; + } + + & .GenericModal__body { + padding: 0; + } +} diff --git a/webapp/channels/src/components/menu/menu.tsx b/webapp/channels/src/components/menu/menu.tsx index 76f0a8d2a1..48b20602d1 100644 --- a/webapp/channels/src/components/menu/menu.tsx +++ b/webapp/channels/src/components/menu/menu.tsx @@ -1,7 +1,10 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import MuiMenu from '@mui/material/Menu'; import MuiMenuList from '@mui/material/MenuList'; +import type {PopoverOrigin} from '@mui/material/Popover'; +import classNames from 'classnames'; import React, { useState, useEffect, @@ -28,7 +31,8 @@ import Constants, {A11yClassNames} from 'utils/constants'; import {isKeyPressed} from 'utils/keyboard'; import {MenuContext, useMenuContextValue} from './menu_context'; -import {MuiMenuStyled} from './menu_styled'; + +import './menu.scss'; const MENU_OPEN_ANIMATION_DURATION = 150; const MENU_CLOSE_ANIMATION_DURATION = 100; @@ -37,6 +41,7 @@ type MenuButtonProps = { id: string; dateTestId?: string; 'aria-label'?: string; + 'aria-describedby'?: string; disabled?: boolean; class?: string; as?: keyof JSX.IntrinsicElements; @@ -55,18 +60,15 @@ type MenuProps = { 'aria-label'?: string; /** - * @warning Make the styling of your components such a way that they dont need this handler + * @warning Make the styling of your components such a way that they don't need this handler */ onToggle?: (isOpen: boolean) => void; onKeyDown?: (event: KeyboardEvent, forceCloseMenu?: () => void) => void; width?: string; } -const defaultAnchorOrigin = {vertical: 'bottom', horizontal: 'left'}; -const defaultTransformOrigin = {vertical: 'top', horizontal: 'left'}; - -type VerticalOrigin = 'top' | 'center' | 'bottom'; -type HorizontalOrigin = 'left' | 'center' | 'right'; +const defaultAnchorOrigin = {vertical: 'bottom', horizontal: 'left'} as PopoverOrigin; +const defaultTransformOrigin = {vertical: 'top', horizontal: 'left'} as PopoverOrigin; interface Props { menuButton: MenuButtonProps; @@ -76,14 +78,8 @@ interface Props { // Use MUI Anchor Playgroup to try various anchorOrigin // and transformOrigin values - https://mui.com/material-ui/react-popover/#anchor-playground - anchorOrigin?: { - vertical: VerticalOrigin; - horizontal: HorizontalOrigin; - }; - transformOrigin?: { - vertical: VerticalOrigin; - horizontal: HorizontalOrigin; - }; + anchorOrigin?: PopoverOrigin; + transformOrigin?: PopoverOrigin; } /** @@ -107,7 +103,7 @@ export function Menu(props: Props) { const [disableAutoFocusItem, setDisableAutoFocusItem] = useState(false); const isMenuOpen = Boolean(anchorElement); - // Callback funtion handler called when menu is closed by escapeKeyDown, backdropClick or tabKeyDown + // Callback function handler called when menu is closed by escapeKeyDown, backdropClick or tabKeyDown function handleMenuClose(event: MouseEvent) { event.preventDefault(); setAnchorElement(null); @@ -125,9 +121,9 @@ export function Menu(props: Props) { setAnchorElement(null); } - // Stop sythetic events from bubbling up to the parent + // Stop synthetic events from bubbling up to the parent // @see https://github.com/mui/material-ui/issues/32064 - function handleMenuClick(e: MouseEvent | KeyboardEvent) { + function handleMenuClick(e: MouseEvent | KeyboardEvent) { e.stopPropagation(); } @@ -193,7 +189,8 @@ export function Menu(props: Props) { aria-haspopup={true} aria-expanded={isMenuOpen} disabled={props.menuButton?.disabled ?? false} - aria-label={props.menuButton?.['aria-label'] ?? ''} + aria-label={props.menuButton?.['aria-label']} + aria-describedby={props.menuButton?.['aria-describedby']} className={props.menuButton?.class ?? ''} onClick={handleMenuButtonClick} onMouseDown={handleMenuButtonMouseDown} @@ -234,21 +231,23 @@ export function Menu(props: Props) { {renderMenuButton()} - {props.children} - + ); diff --git a/webapp/channels/src/components/menu/menu_item.tsx b/webapp/channels/src/components/menu/menu_item.tsx index a3f766f102..120d36c0bc 100644 --- a/webapp/channels/src/components/menu/menu_item.tsx +++ b/webapp/channels/src/components/menu/menu_item.tsx @@ -89,6 +89,25 @@ export interface Props extends MuiMenuItemProps { children?: ReactNode; } +/** + * The props for the first menu item to be passed in. + * @example + * + * <-- Container passes the props to the first item + * + * + */ +export type FirstMenuItemProps = Omit< +Props, +| 'onClick' +| 'leadingElement' +| 'labels' +| 'trailingElements' +| 'isDestructive' +| 'isLabelsRowLayout' +| 'children' +>; + /** * To be used as a child of Menu component. * Checkout Compass's Menu Item(compass.mattermost.com) for terminology, styling and usage guidelines. @@ -161,7 +180,11 @@ export function MenuItem(props: Props) { } // When both primary and secondary labels are passed, we need to apply minor changes to the styling. Check below in styled component for more details. - const hasSecondaryLabel = labels && labels.props && labels.props.children && Children.count(labels.props.children) === 2; + // we count after converting to array as it removes falsy values from labels.props.children + const hasSecondaryLabel = labels && + labels.props && + labels.props.children && + Children.count(Children.toArray(labels.props.children)) === 2; return ( prop !== 'asSubMenu', -})( - ({asSubMenu, width}) => ({ - '& .MuiPaper-root': { - backgroundColor: 'var(--center-channel-bg)', - boxShadow: `${ - asSubMenu ? 'var(--elevation-5)' : 'var(--elevation-4)' - }, 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.12) inset`, - minWidth: '114px', - maxWidth: '496px', - maxHeight: '80vh', - width, - }, - }), -); diff --git a/webapp/channels/src/components/menu/sub_menu.scss b/webapp/channels/src/components/menu/sub_menu.scss deleted file mode 100644 index b30d419b5c..0000000000 --- a/webapp/channels/src/components/menu/sub_menu.scss +++ /dev/null @@ -1,11 +0,0 @@ -.menuModal { - & .modal-header { - & > .close { - visibility: hidden; - } - } - - & .GenericModal__body { - padding: 0; - } -} diff --git a/webapp/channels/src/components/menu/sub_menu.tsx b/webapp/channels/src/components/menu/sub_menu.tsx index 26e3e182c1..a54111f5ea 100644 --- a/webapp/channels/src/components/menu/sub_menu.tsx +++ b/webapp/channels/src/components/menu/sub_menu.tsx @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import MuiMenu from '@mui/material/Menu'; import MuiMenuList from '@mui/material/MenuList'; import type {PopoverOrigin} from '@mui/material/Popover'; import React, { @@ -31,9 +32,6 @@ import {isKeyPressed} from 'utils/keyboard'; import {SubMenuContext, useMenuContextValue} from './menu_context'; import {MenuItem} from './menu_item'; import type {Props as MenuItemProps} from './menu_item'; -import {MuiMenuStyled} from './menu_styled'; - -import './sub_menu.scss'; interface Props { id: MenuItemProps['id']; @@ -41,10 +39,12 @@ interface Props { labels: MenuItemProps['labels']; trailingElements?: MenuItemProps['trailingElements']; isDestructive?: MenuItemProps['isDestructive']; + role?: MenuItemProps['role']; // Menu props menuId: string; menuAriaLabel?: string; + menuAriaDescribedBy?: string; forceOpenOnLeft?: boolean; // Most of the times this is not needed, since submenu position is calculated and placed children: ReactNode; @@ -57,8 +57,10 @@ export function SubMenu(props: Props) { labels, trailingElements, isDestructive, + role, menuId, menuAriaLabel, + menuAriaDescribedBy, forceOpenOnLeft, children, ...rest @@ -148,6 +150,7 @@ export function SubMenu(props: Props) { labels, trailingElements, isDestructive, + role, onClick: isMobileView ? handleOnClick : undefined, // OnClicks on parent menuItem of subMenu is only needed in mobile view }; @@ -163,13 +166,13 @@ export function SubMenu(props: Props) { onMouseLeave={handleMouseLeave} onKeyDown={handleKeyDown} > - {/* This component is needed here to re enable pointer events for the submenu items which we had to disable above as */} {/* pointer turns to default as soon as it leaves the parent menu */} @@ -178,6 +181,7 @@ export function SubMenu(props: Props) { id={menuId} component='ul' aria-label={menuAriaLabel} + aria-describedby={menuAriaDescribedBy} className={A11yClassNames.POPUP} onKeyDown={handleSubMenuKeyDown} sx={{ @@ -190,7 +194,7 @@ export function SubMenu(props: Props) { {children} - + ); } diff --git a/webapp/channels/src/components/onboarding_tasks/complete_your_profile_tour_tip.tsx b/webapp/channels/src/components/onboarding_tasks/complete_your_profile_tour_tip.tsx index b8682c9356..369db76804 100644 --- a/webapp/channels/src/components/onboarding_tasks/complete_your_profile_tour_tip.tsx +++ b/webapp/channels/src/components/onboarding_tasks/complete_your_profile_tour_tip.tsx @@ -43,7 +43,7 @@ export const CompleteYourProfileTour = () => {

); - const overlayPunchOut = useMeasurePunchouts(['status-drop-down-menu-list'], [], {y: -6, height: 6, x: 0, width: 0}); + const overlayPunchOut = useMeasurePunchouts(['userAccountMenu'], [], {y: -6, height: 6, x: 0, width: 0}); const onDismiss = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); @@ -64,6 +64,7 @@ export const CompleteYourProfileTour = () => { singleTip={true} showOptOut={false} interactivePunchOut={true} + zIndex={1058} /> ); }; diff --git a/webapp/channels/src/components/onboarding_tasks/onboarding_tasks_manager.tsx b/webapp/channels/src/components/onboarding_tasks/onboarding_tasks_manager.tsx index 3cca6f7f4e..011346935e 100644 --- a/webapp/channels/src/components/onboarding_tasks/onboarding_tasks_manager.tsx +++ b/webapp/channels/src/components/onboarding_tasks/onboarding_tasks_manager.tsx @@ -20,7 +20,6 @@ import { switchToChannels, } from 'actions/views/onboarding_tasks'; import {setProductMenuSwitcherOpen} from 'actions/views/product_menu'; -import {setStatusDropdown} from 'actions/views/status_dropdown'; import {getOnboardingTaskPreferences} from 'selectors/onboarding'; import Channels from 'components/common/svg_images_components/channels_svg'; @@ -40,6 +39,7 @@ import { } from 'components/tours'; import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants'; +import {clickOnMenuButton, MenuButtonIds} from 'utils/keyboard'; import type {GlobalState} from 'types/store'; @@ -239,7 +239,7 @@ export const useHandleOnBoardingTaskTrigger = () => { break; } case OnboardingTasksName.COMPLETE_YOUR_PROFILE: { - dispatch(setStatusDropdown(true)); + clickOnMenuButton(MenuButtonIds.userAccountMenu); dispatch(setShowOnboardingCompleteProfileTour(true)); handleSaveData(taskName, TaskNameMapToSteps[taskName].STARTED, true); if (inAdminConsole) { diff --git a/webapp/channels/src/components/post_view/post_header_custom_status/post_header_custom_status.tsx b/webapp/channels/src/components/post_view/post_header_custom_status/post_header_custom_status.tsx index 175d1b7526..79231ea9b6 100644 --- a/webapp/channels/src/components/post_view/post_header_custom_status/post_header_custom_status.tsx +++ b/webapp/channels/src/components/post_view/post_header_custom_status/post_header_custom_status.tsx @@ -3,14 +3,15 @@ import React, {useMemo} from 'react'; import {FormattedMessage} from 'react-intl'; -import {useDispatch, useSelector} from 'react-redux'; +import {useSelector} from 'react-redux'; -import {setStatusDropdown} from 'actions/views/status_dropdown'; import {makeGetCustomStatus, showPostHeaderUpdateStatusButton, isCustomStatusEnabled} from 'selectors/views/custom_status'; import CustomStatusEmoji from 'components/custom_status/custom_status_emoji'; import EmojiIcon from 'components/widgets/icons/emoji_icon'; +import {clickOnMenuButton, MenuButtonIds} from 'utils/keyboard'; + import type {GlobalState} from 'types/store'; interface ComponentProps { @@ -22,7 +23,6 @@ interface ComponentProps { const PostHeaderCustomStatus = (props: ComponentProps) => { const getCustomStatus = useMemo(makeGetCustomStatus, []); const {userId, isSystemMessage, isBot} = props; - const dispatch = useDispatch(); const userCustomStatus = useSelector((state: GlobalState) => getCustomStatus(state, userId)); const showUpdateStatusButton = useSelector(showPostHeaderUpdateStatusButton); const customStatusEnabled = useSelector(isCustomStatusEnabled); @@ -51,7 +51,7 @@ const PostHeaderCustomStatus = (props: ComponentProps) => { const updateStatus = (event: React.MouseEvent) => { event.preventDefault(); - dispatch(setStatusDropdown(true)); + clickOnMenuButton(MenuButtonIds.userAccountMenu); }; return ( diff --git a/webapp/channels/src/components/status_dropdown/__snapshots__/status_dropdown.test.tsx.snap b/webapp/channels/src/components/status_dropdown/__snapshots__/status_dropdown.test.tsx.snap deleted file mode 100644 index ebe0286e81..0000000000 --- a/webapp/channels/src/components/status_dropdown/__snapshots__/status_dropdown.test.tsx.snap +++ /dev/null @@ -1,2552 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/StatusDropdown should match snapshot in default state 1`] = ` - - - - -
- - Nev Aa - - - @undefined - -
-
- - - } - onClick={[Function]} - rightDecorator={ - - } - show={false} - text="Out of office" - /> - - - - } - id="status-menu-online" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Online" - /> - - } - id="status-menu-away" - onClick={[Function]} - rightDecorator={ - - } - show={true} - text="Away" - /> - - } - id="status-menu-dnd" - openUp={false} - rightDecorator={false} - subMenu={ - Array [ - Object { - "direction": "right", - "id": "dndSubMenu-header", - "isHeader": true, - "text": "Clear after:", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-dont_clear", - "text": "Don't clear", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-thirty_minutes", - "text": "30 mins", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-one_hour", - "text": "1 hour", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-two_hours", - "text": "2 hours", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-tomorrow", - "text": - Tomorrow - - - , - - - , - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-custom", - "text": "Choose date and time", - }, - ] - } - text="Do not disturb" - /> - - } - id="status-menu-offline" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Offline" - /> - - - - } - id="accountSettings" - modalId="user_settings" - show={true} - text="Profile" - /> - - - - - } - id="logout" - onClick={[Function]} - show={true} - text="Log Out" - /> - - -
-
-`; - -exports[`components/StatusDropdown should match snapshot with custom status and expiry 1`] = ` - - - - -
- - Nev Aa - - - @undefined - -
-
- - - } - onClick={[Function]} - rightDecorator={ - - } - show={false} - text="Out of office" - /> - - - - - - - - - - - -
- - } - > - - -
-
- -
-
- - - } - id="status-menu-online" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Online" - /> - - } - id="status-menu-away" - onClick={[Function]} - rightDecorator={ - - } - show={true} - text="Away" - /> - - } - id="status-menu-dnd" - openUp={false} - rightDecorator={false} - subMenu={ - Array [ - Object { - "direction": "right", - "id": "dndSubMenu-header", - "isHeader": true, - "text": "Clear after:", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-dont_clear", - "text": "Don't clear", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-thirty_minutes", - "text": "30 mins", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-one_hour", - "text": "1 hour", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-two_hours", - "text": "2 hours", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-tomorrow", - "text": - Tomorrow - - - , - - - , - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-custom", - "text": "Choose date and time", - }, - ] - } - text="Do not disturb" - /> - - } - id="status-menu-offline" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Offline" - /> - - - - } - id="accountSettings" - modalId="user_settings" - show={true} - text="Profile" - /> - - - - - } - id="logout" - onClick={[Function]} - show={true} - text="Log Out" - /> - - -
-
-`; - -exports[`components/StatusDropdown should match snapshot with custom status enabled 1`] = ` - - - - -
- - Nev Aa - - - @undefined - -
-
- - - } - onClick={[Function]} - rightDecorator={ - - } - show={false} - text="Out of office" - /> - - - - - - - - - - Set a custom status - - - - - - - } - id="status-menu-online" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Online" - /> - - } - id="status-menu-away" - onClick={[Function]} - rightDecorator={ - - } - show={true} - text="Away" - /> - - } - id="status-menu-dnd" - openUp={false} - rightDecorator={false} - subMenu={ - Array [ - Object { - "direction": "right", - "id": "dndSubMenu-header", - "isHeader": true, - "text": "Clear after:", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-dont_clear", - "text": "Don't clear", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-thirty_minutes", - "text": "30 mins", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-one_hour", - "text": "1 hour", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-two_hours", - "text": "2 hours", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-tomorrow", - "text": - Tomorrow - - - , - - - , - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-custom", - "text": "Choose date and time", - }, - ] - } - text="Do not disturb" - /> - - } - id="status-menu-offline" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Offline" - /> - - - - } - id="accountSettings" - modalId="user_settings" - show={true} - text="Profile" - /> - - - - - } - id="logout" - onClick={[Function]} - show={true} - text="Log Out" - /> - - -
-
-`; - -exports[`components/StatusDropdown should match snapshot with custom status expired 1`] = ` - - - - -
- - Nev Aa - - - @undefined - -
-
- - - } - onClick={[Function]} - rightDecorator={ - - } - show={false} - text="Out of office" - /> - - - - - - - - - - Set a custom status - - - - - - - } - id="status-menu-online" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Online" - /> - - } - id="status-menu-away" - onClick={[Function]} - rightDecorator={ - - } - show={true} - text="Away" - /> - - } - id="status-menu-dnd" - openUp={false} - rightDecorator={false} - subMenu={ - Array [ - Object { - "direction": "right", - "id": "dndSubMenu-header", - "isHeader": true, - "text": "Clear after:", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-dont_clear", - "text": "Don't clear", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-thirty_minutes", - "text": "30 mins", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-one_hour", - "text": "1 hour", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-two_hours", - "text": "2 hours", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-tomorrow", - "text": - Tomorrow - - - , - - - , - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-custom", - "text": "Choose date and time", - }, - ] - } - text="Do not disturb" - /> - - } - id="status-menu-offline" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Offline" - /> - - - - } - id="accountSettings" - modalId="user_settings" - show={true} - text="Profile" - /> - - - - - } - id="logout" - onClick={[Function]} - show={true} - text="Log Out" - /> - - -
-
-`; - -exports[`components/StatusDropdown should match snapshot with custom status pulsating dot enabled 1`] = ` - - - - -
- - Nev Aa - - - @undefined - -
-
- - - } - onClick={[Function]} - rightDecorator={ - - } - show={false} - text="Out of office" - /> - - - - - - - - - - Set a custom status - - - - - - - - } - id="status-menu-online" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Online" - /> - - } - id="status-menu-away" - onClick={[Function]} - rightDecorator={ - - } - show={true} - text="Away" - /> - - } - id="status-menu-dnd" - openUp={false} - rightDecorator={false} - subMenu={ - Array [ - Object { - "direction": "right", - "id": "dndSubMenu-header", - "isHeader": true, - "text": "Clear after:", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-dont_clear", - "text": "Don't clear", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-thirty_minutes", - "text": "30 mins", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-one_hour", - "text": "1 hour", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-two_hours", - "text": "2 hours", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-tomorrow", - "text": - Tomorrow - - - , - - - , - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-custom", - "text": "Choose date and time", - }, - ] - } - text="Do not disturb" - /> - - } - id="status-menu-offline" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Offline" - /> - - - - } - id="accountSettings" - modalId="user_settings" - show={true} - text="Profile" - /> - - - - - } - id="logout" - onClick={[Function]} - show={true} - text="Log Out" - /> - - -
-
-`; - -exports[`components/StatusDropdown should match snapshot with profile picture URL 1`] = ` - - - - - -
- - Nev Aa - - - @undefined - -
-
- - - } - onClick={[Function]} - rightDecorator={ - - } - show={false} - text="Out of office" - /> - - - - } - id="status-menu-online" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Online" - /> - - } - id="status-menu-away" - onClick={[Function]} - rightDecorator={ - - } - show={true} - text="Away" - /> - - } - id="status-menu-dnd" - openUp={false} - rightDecorator={false} - subMenu={ - Array [ - Object { - "direction": "right", - "id": "dndSubMenu-header", - "isHeader": true, - "text": "Clear after:", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-dont_clear", - "text": "Don't clear", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-thirty_minutes", - "text": "30 mins", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-one_hour", - "text": "1 hour", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-two_hours", - "text": "2 hours", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-tomorrow", - "text": - Tomorrow - - - , - - - , - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-custom", - "text": "Choose date and time", - }, - ] - } - text="Do not disturb" - /> - - } - id="status-menu-offline" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Offline" - /> - - - - } - id="accountSettings" - modalId="user_settings" - show={true} - text="Profile" - /> - - - - - } - id="logout" - onClick={[Function]} - show={true} - text="Log Out" - /> - - -
-
-`; - -exports[`components/StatusDropdown should match snapshot with status dropdown open 1`] = ` - - - - -
- - Nev Aa - - - @undefined - -
-
- - - } - onClick={[Function]} - rightDecorator={ - - } - show={false} - text="Out of office" - /> - - - - } - id="status-menu-online" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Online" - /> - - } - id="status-menu-away" - onClick={[Function]} - rightDecorator={ - - } - show={true} - text="Away" - /> - - } - id="status-menu-dnd" - openUp={false} - rightDecorator={false} - subMenu={ - Array [ - Object { - "direction": "right", - "id": "dndSubMenu-header", - "isHeader": true, - "text": "Clear after:", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-dont_clear", - "text": "Don't clear", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-thirty_minutes", - "text": "30 mins", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-one_hour", - "text": "1 hour", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-two_hours", - "text": "2 hours", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-tomorrow", - "text": - Tomorrow - - - , - - - , - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-custom", - "text": "Choose date and time", - }, - ] - } - text="Do not disturb" - /> - - } - id="status-menu-offline" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Offline" - /> - - - - } - id="accountSettings" - modalId="user_settings" - show={true} - text="Profile" - /> - - - - - } - id="logout" - onClick={[Function]} - show={true} - text="Log Out" - /> - - -
-
-`; - -exports[`components/StatusDropdown should not show clear status button when custom status is not set 1`] = ` - - - - -
- - Nev Aa - - - @undefined - -
-
- - - } - onClick={[Function]} - rightDecorator={ - - } - show={false} - text="Out of office" - /> - - - - - - - - - - Set a custom status - - - - - - - } - id="status-menu-online" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Online" - /> - - } - id="status-menu-away" - onClick={[Function]} - rightDecorator={ - - } - show={true} - text="Away" - /> - - } - id="status-menu-dnd" - openUp={false} - rightDecorator={false} - subMenu={ - Array [ - Object { - "direction": "right", - "id": "dndSubMenu-header", - "isHeader": true, - "text": "Clear after:", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-dont_clear", - "text": "Don't clear", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-thirty_minutes", - "text": "30 mins", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-one_hour", - "text": "1 hour", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-two_hours", - "text": "2 hours", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-tomorrow", - "text": - Tomorrow - - - , - - - , - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-custom", - "text": "Choose date and time", - }, - ] - } - text="Do not disturb" - /> - - } - id="status-menu-offline" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Offline" - /> - - - - } - id="accountSettings" - modalId="user_settings" - show={true} - text="Profile" - /> - - - - - } - id="logout" - onClick={[Function]} - show={true} - text="Log Out" - /> - - -
-
-`; - -exports[`components/StatusDropdown should show clear status button when custom status is set 1`] = ` - - - - -
- - Nev Aa - - - @undefined - -
-
- - - } - onClick={[Function]} - rightDecorator={ - - } - show={false} - text="Out of office" - /> - - - - - - - - - - - -
- - } - > - - -
-
- -
-
- - - } - id="status-menu-online" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Online" - /> - - } - id="status-menu-away" - onClick={[Function]} - rightDecorator={ - - } - show={true} - text="Away" - /> - - } - id="status-menu-dnd" - openUp={false} - rightDecorator={false} - subMenu={ - Array [ - Object { - "direction": "right", - "id": "dndSubMenu-header", - "isHeader": true, - "text": "Clear after:", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-dont_clear", - "text": "Don't clear", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-thirty_minutes", - "text": "30 mins", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-one_hour", - "text": "1 hour", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-two_hours", - "text": "2 hours", - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-tomorrow", - "text": - Tomorrow - - - , - - - , - }, - Object { - "action": [Function], - "direction": "right", - "id": "dndTime-custom", - "text": "Choose date and time", - }, - ] - } - text="Do not disturb" - /> - - } - id="status-menu-offline" - onClick={[Function]} - rightDecorator={false} - show={true} - text="Offline" - /> - - - - } - id="accountSettings" - modalId="user_settings" - show={true} - text="Profile" - /> - - - - - } - id="logout" - onClick={[Function]} - show={true} - text="Log Out" - /> - - -
-
-`; diff --git a/webapp/channels/src/components/status_dropdown/index.ts b/webapp/channels/src/components/status_dropdown/index.ts deleted file mode 100644 index 7a92fe9d58..0000000000 --- a/webapp/channels/src/components/status_dropdown/index.ts +++ /dev/null @@ -1,73 +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 {setStatus, unsetCustomStatus} from 'mattermost-redux/actions/users'; -import {Client4} from 'mattermost-redux/client'; -import {Preferences} from 'mattermost-redux/constants'; -import {get, getBool, getInt} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; -import {getCurrentUser, getDndEndTimeForUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; - -import {openModal} from 'actions/views/modals'; -import {setStatusDropdown} from 'actions/views/status_dropdown'; -import {makeGetCustomStatus, isCustomStatusEnabled, showStatusDropdownPulsatingDot, isCustomStatusExpired} from 'selectors/views/custom_status'; -import {isStatusDropdownOpen} from 'selectors/views/status_dropdown'; - -import { - OnboardingTaskCategory, - OnboardingTasksName, - TaskNameMapToSteps, -} from 'components/onboarding_tasks'; - -import type {GlobalState} from 'types/store'; - -import StatusDropdown from './status_dropdown'; - -function makeMapStateToProps() { - const getCustomStatus = makeGetCustomStatus(); - - return function mapStateToProps(state: GlobalState) { - const currentUser = getCurrentUser(state); - - const userId = currentUser?.id; - const customStatus = getCustomStatus(state, userId); - const isMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false); - const step = getInt(state, OnboardingTaskCategory, OnboardingTasksName.COMPLETE_YOUR_PROFILE, 0); - const showCompleteYourProfileTour = step === TaskNameMapToSteps[OnboardingTasksName.COMPLETE_YOUR_PROFILE].STARTED; - return { - userId, - profilePicture: Client4.getProfilePictureUrl(userId, currentUser?.last_picture_update), - autoResetPref: get(state, Preferences.CATEGORY_AUTO_RESET_MANUAL_STATUS, userId, ''), - status: getStatusForUserId(state, userId), - customStatus, - currentUser, - isCustomStatusEnabled: isCustomStatusEnabled(state), - isCustomStatusExpired: isCustomStatusExpired(state, customStatus), - isMilitaryTime, - isStatusDropdownOpen: isStatusDropdownOpen(state), - showCustomStatusPulsatingDot: showStatusDropdownPulsatingDot(state), - showCompleteYourProfileTour, - timezone: getCurrentTimezone(state), - dndEndTime: getDndEndTimeForUserId(state, userId), - }; - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - actions: bindActionCreators({ - openModal, - setStatus, - unsetCustomStatus, - setStatusDropdown, - savePreferences, - }, dispatch), - }; -} - -export default connect(makeMapStateToProps, mapDispatchToProps)(StatusDropdown); diff --git a/webapp/channels/src/components/status_dropdown/status_dropdown.scss b/webapp/channels/src/components/status_dropdown/status_dropdown.scss deleted file mode 100644 index 9a7c27c652..0000000000 --- a/webapp/channels/src/components/status_dropdown/status_dropdown.scss +++ /dev/null @@ -1,358 +0,0 @@ -.logout__icon { - &:hover { - fill: rgba(255, 0, 0, 0.75); - } -} - -#statusDropdownMenu { - .icon { - position: relative; - display: inline-flex; - width: 24px; - height: 24px; - justify-content: center; - padding: 0; - margin: 0 8px 0 0; - - span { - display: flex; - } - - svg { - width: 16px; - height: 16px; - } - } - - #status-menu-dnd { - padding: 4px 16px; - - .dropdown-menu { - margin: auto; - - #dndSubMenu-header { - font-weight: bold; - pointer-events: none; - } - } - - .icon { - top: 2px; - } - - .SubMenu__icon-right { - padding-top: 5px; - } - } -} - -#status-menu-dnd_menuitem { - .SubMenuItem.MenuItem { - flex-grow: 1; - } - - .SubMenuItemContainer { - &:hover, - &:focus { - background-color: unset; - } - } - - #dndTime-tomorrow .dndTime-tomorrow_timestamp { - &::before { - content: "\00a0"; - } - - margin-left: auto; - opacity: 0.7; - } -} - -button.custom_status__row { - position: relative; - height: auto; - font-size: inherit; - - &.flex { - display: flex; - } - - &:hover { - background-color: rgba(var(--center-channel-color-rgb), 0.08); - - .custom-status-emoji { - fill: rgba(var(--center-channel-color-rgb), 0.75); - } - } - - &:active { - background-color: rgba(var(--button-bg-rgb), 0.08); - } -} - -.custom_status { - &__container { - display: flex; - padding: 7px 0; - color: rgba(var(--center-channel-color-rgb), 1); - line-height: 20px; - } - - &__icon { - margin-right: 12px; - } - - &__text { - overflow: hidden; - max-width: 73%; - height: 16px; - line-height: 16px; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__expiry { - font-size: 12px; - line-height: 16px; - opacity: 0.7; - - &.padded { - display: block; - padding-left: 28px; - } - } -} - -#custom_status__clear { - position: absolute; - top: 0; - right: 10px; - width: auto; - padding: 0; - color: rgba(var(--center-channel-color-rgb), 0.52); - - &:hover { - background: transparent; - } -} - -.custom-status { - &-emoji { - fill: rgba(var(--center-channel-color-rgb), 0.75); - - &:hover { - fill: rgba(var(--center-channel-color-rgb), 0.88); - } - } - - &-suggestion-emoji > span:first-child { - background-size: 20px; - } -} - -.status-dropdown-menu-global-header { - padding: 4px 8px; - border-radius: 4px; - - &:hover { - background: var(--sidebar-text-08); - cursor: pointer; - - .status { - background-image: - linear-gradient( - 0deg, - rgba(var(--global-header-text-rgb), 0.08), - rgba(var(--global-header-text-rgb), 0.08) - ); - } - } - - &.active { - background: rgba(var(--global-header-text-rgb), 0.16); - - .status { - background-image: - linear-gradient( - 0deg, - rgba(var(--global-header-text-rgb), 0.16), - rgba(var(--global-header-text-rgb), 0.16) - ); - } - } - - .Menu__content { - right: -1px; - left: inherit; - width: 264px; - } - - .custom_status__row > #custom_status__clear { - top: 4px; - } - - .custom_status__container { - align-items: center; - color: rgba(var(--center-channel-color-rgb), 0.75); - line-height: 14px; - } - - .username-wrapper { - padding: 0 12px; - - > p { - overflow: hidden; - max-width: 150px; - text-overflow: ellipsis; - white-space: nowrap; - - &.contrast { - color: rgba(var(--center-channel-color-rgb), 0.75); - } - - &.bold { - font-family: 'Metropolis', sans-serif; - font-size: 16px; - font-weight: 600; - } - } - } - - .MenuHeader { - height: 50px; - padding: 0 16px; - cursor: pointer; - - &:hover { - background-color: rgba(var(--center-channel-color-rgb), 0.1); - } - } - - .MenuItem > button, - #status-menu-dnd { - padding: 1px 16px; - - .flex { - display: flex; - } - - .MenuItem__primary-text, - .grid { - display: grid; - align-items: center; - grid-template-columns: auto 1fr auto auto auto; - - .SubMenu__icon-right { - padding-top: 0; - padding-left: 12px; - } - } - - > .MenuItem__help-text { - padding-left: 28px; - } - } - - #statusDropdownMenu, - #statusDropdownMenu #status-menu-dnd { - .icon { - top: unset; - width: auto; - height: auto; - padding: 0 12px 0 0; - margin: 0; - } - - .SubMenu__icon-right { - padding-top: 0; - } - } - - .dnd--icon { - vertical-align: middle; - } - - .status-wrapper { - display: flex; - height: auto; - align-items: center; - - .status { - position: absolute; - top: auto; - right: -4px; - bottom: -4px; - display: inline-flex; - width: 16px; - height: 16px; - align-items: center; - justify-content: center; - background-color: var(--global-header-background); - color: var(--global-header-text); - - &:hover { - cursor: pointer; - } - - svg { - top: 0; - } - } - } -} - -@keyframes pulse1 { - 0% { - opacity: 1; - transform: scale(1); - } - - 80% { - opacity: 0; - transform: scale(2.5); - } - - 100% { - opacity: 0; - transform: scale(2.5); - } -} - -@keyframes pulse2 { - 0% { - opacity: 1; - transform: scale(1); - } - - 30% { - opacity: 1; - transform: scale(1); - } - - 100% { - opacity: 0; - transform: scale(2.5); - } -} - -.status-dropdpwn-menu .status-dropdown-menu__clear-container .input-clear { - position: absolute; - color: rgba(var(--center-channel-color-rgb), 0.64); - opacity: 1; -} - -.status-dropdpwn-account-settings { - position: relative; -} - -#accountSettings { - position: relative; -} - -.account-settings-complete { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; -} diff --git a/webapp/channels/src/components/status_dropdown/status_dropdown.test.tsx b/webapp/channels/src/components/status_dropdown/status_dropdown.test.tsx deleted file mode 100644 index 5ed2737b0c..0000000000 --- a/webapp/channels/src/components/status_dropdown/status_dropdown.test.tsx +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {CustomStatusDuration} from '@mattermost/types/users'; -import type {UserProfile} from '@mattermost/types/users'; - -import {fakeDate} from 'tests/helpers/date'; -import {shallowWithIntl} from 'tests/helpers/intl-test-helper'; - -import StatusDropdown from './status_dropdown'; - -describe('components/StatusDropdown', () => { - let resetFakeDate: () => void; - - beforeEach(() => { - resetFakeDate = fakeDate(new Date('2021-11-02T22:48:57Z')); - }); - - afterEach(() => { - resetFakeDate(); - }); - - const actions = { - openModal: jest.fn(), - setStatus: jest.fn(), - unsetCustomStatus: jest.fn(), - setStatusDropdown: jest.fn(), - savePreferences: jest.fn(), - }; - - const baseProps = { - actions, - userId: '', - currentUser: { - id: 'user_id', - first_name: 'Nev', - last_name: 'Aa', - } as UserProfile, - userTimezone: { - useAutomaticTimezone: 'true', - automaticTimezone: 'America/New_York', - manualTimezone: '', - }, - status: 'away', - isMilitaryTime: false, - isCustomStatusEnabled: false, - isCustomStatusExpired: false, - isStatusDropdownOpen: false, - showCustomStatusPulsatingDot: false, - showCompleteYourProfileTour: false, - }; - - test('should match snapshot in default state', () => { - const wrapper = shallowWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot with profile picture URL', () => { - const props = { - ...baseProps, - profilePicture: 'http://localhost:8065/api/v4/users/jsx5jmdiyjyuzp9rzwfaf5pwjo/image?_=1590519110944', - }; - - const wrapper = shallowWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot with status dropdown open', () => { - const props = { - ...baseProps, - isStatusDropdownOpen: true, - }; - - const wrapper = shallowWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot with custom status enabled', () => { - const props = { - ...baseProps, - isStatusDropdownOpen: true, - isCustomStatusEnabled: true, - }; - - const wrapper = shallowWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot with custom status pulsating dot enabled', () => { - const props = { - ...baseProps, - isStatusDropdownOpen: true, - isCustomStatusEnabled: true, - showCustomStatusPulsatingDot: true, - }; - - const wrapper = shallowWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot with custom status and expiry', () => { - const customStatus = { - emoji: 'calendar', - text: 'In a meeting', - duration: CustomStatusDuration.TODAY, - expires_at: '2021-05-03T23:59:59.000Z', - }; - const props = { - ...baseProps, - isStatusDropdownOpen: true, - isCustomStatusEnabled: true, - customStatus, - }; - - const wrapper = shallowWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot with custom status expired', () => { - const customStatus = { - emoji: 'calendar', - text: 'In a meeting', - duration: CustomStatusDuration.TODAY, - expires_at: '2021-05-03T23:59:59.000Z', - }; - const props = { - ...baseProps, - isStatusDropdownOpen: true, - isCustomStatusEnabled: true, - isCustomStatusExpired: true, - customStatus, - }; - - const wrapper = shallowWithIntl( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('should show clear status button when custom status is set', () => { - const customStatus = { - emoji: 'calendar', - text: 'In a meeting', - duration: CustomStatusDuration.TODAY, - expires_at: '2021-05-03T23:59:59.000Z', - }; - const props = { - ...baseProps, - isStatusDropdownOpen: true, - isCustomStatusEnabled: true, - isCustomStatusExpired: false, - customStatus, - }; - - const wrapper = shallowWithIntl( - , - ); - - expect(wrapper.find('.status-dropdown-menu__clear-container').exists()).toBe(true); - expect(wrapper).toMatchSnapshot(); - }); - - test('should not show clear status button when custom status is not set', () => { - const customStatus = undefined; - const props = { - ...baseProps, - isStatusDropdownOpen: true, - isCustomStatusEnabled: true, - isCustomStatusExpired: false, - customStatus, - }; - - const wrapper = shallowWithIntl( - , - ); - - expect(wrapper.find('.status-dropdown-menu__clear-container').exists()).toBe(false); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/webapp/channels/src/components/status_dropdown/status_dropdown.tsx b/webapp/channels/src/components/status_dropdown/status_dropdown.tsx deleted file mode 100644 index 7543f54402..0000000000 --- a/webapp/channels/src/components/status_dropdown/status_dropdown.tsx +++ /dev/null @@ -1,701 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import classNames from 'classnames'; -import moment from 'moment-timezone'; -import React from 'react'; -import type {ReactNode} from 'react'; -import {injectIntl, FormattedDate, FormattedMessage, FormattedTime, defineMessage, defineMessages} from 'react-intl'; -import type {IntlShape, MessageDescriptor} from 'react-intl'; - -import StatusIcon from '@mattermost/compass-components/components/status-icon'; // eslint-disable-line no-restricted-imports -import Text from '@mattermost/compass-components/components/text'; // eslint-disable-line no-restricted-imports -import type {TUserStatus} from '@mattermost/compass-components/shared'; // eslint-disable-line no-restricted-imports -import {AccountOutlineIcon, CancelIcon, CheckIcon, ExitToAppIcon} from '@mattermost/compass-icons/components'; -import {PulsatingDot} from '@mattermost/components'; -import type {PreferenceType} from '@mattermost/types/preferences'; -import {CustomStatusDuration} from '@mattermost/types/users'; -import type {UserCustomStatus, UserProfile, UserStatus} from '@mattermost/types/users'; - -import * as GlobalActions from 'actions/global_actions'; - -import CustomStatusEmoji from 'components/custom_status/custom_status_emoji'; -import CustomStatusModal from 'components/custom_status/custom_status_modal'; -import CustomStatusText from 'components/custom_status/custom_status_text'; -import ExpiryTime from 'components/custom_status/expiry_time'; -import DndCustomTimePicker from 'components/dnd_custom_time_picker_modal'; -import {OnboardingTaskCategory, OnboardingTasksName, TaskNameMapToSteps, CompleteYourProfileTour} from 'components/onboarding_tasks'; -import ResetStatusModal from 'components/reset_status_modal'; -import UserSettingsModal from 'components/user_settings/modal'; -import EmojiIcon from 'components/widgets/icons/emoji_icon'; -import Menu from 'components/widgets/menu/menu'; -import MenuWrapper from 'components/widgets/menu/menu_wrapper'; -import Avatar from 'components/widgets/users/avatar/avatar'; -import type {TAvatarSizeToken} from 'components/widgets/users/avatar/avatar'; -import WithTooltip from 'components/with_tooltip'; - -import {ModalIdentifiers, UserStatuses} from 'utils/constants'; -import {getBrowserTimezone, getCurrentDateTimeForTimezone, getCurrentMomentForTimezone} from 'utils/timezone'; - -import type {ModalData} from 'types/actions'; -import type {Menu as MenuType} from 'types/store/plugins'; - -import './status_dropdown.scss'; - -type Props = { - intl: IntlShape; - status?: string; - userId: string; - profilePicture?: string; - autoResetPref?: string; - actions: { - openModal:

(modalData: ModalData

) => void; - setStatus: (status: UserStatus) => void; - unsetCustomStatus: () => void; - savePreferences: (userId: string, preferences: PreferenceType[]) => void; - setStatusDropdown: (open: boolean) => void; - }; - customStatus?: UserCustomStatus; - currentUser: UserProfile; - isCustomStatusEnabled: boolean; - isCustomStatusExpired: boolean; - isMilitaryTime: boolean; - isStatusDropdownOpen: boolean; - showCompleteYourProfileTour: boolean; - showCustomStatusPulsatingDot: boolean; - timezone?: string; - dndEndTime?: number; -} - -type State = { - openUp: boolean; - width: number; - isStatusSet: boolean; -}; - -export const statusDropdownMessages: Record> = { - ooo: defineMessages({ - name: { - id: 'status_dropdown.set_ooo', - defaultMessage: 'Out of office', - }, - extra: { - id: 'status_dropdown.set_ooo.extra', - defaultMessage: 'Automatic Replies are enabled', - }, - }), - online: defineMessages({ - name: { - id: 'status_dropdown.set_online', - defaultMessage: 'Online', - }, - }), - away: defineMessages({ - name: { - id: 'status_dropdown.set_away', - defaultMessage: 'Away', - }, - }), - dnd: defineMessages({ - name: { - id: 'status_dropdown.set_dnd', - defaultMessage: 'Do not disturb', - }, - }), - offline: defineMessages({ - name: { - id: 'status_dropdown.set_offline', - defaultMessage: 'Offline', - }, - }), -}; - -export class StatusDropdown extends React.PureComponent { - dndTimes = [ - {id: 'dont_clear', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.dont_clear', defaultMessage: 'Don\'t clear'})}, - {id: 'thirty_minutes', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.thirty_minutes', defaultMessage: '30 mins'})}, - {id: 'one_hour', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.one_hour', defaultMessage: '1 hour'})}, - {id: 'two_hours', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.two_hours', defaultMessage: '2 hours'})}, - {id: 'tomorrow', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.tomorrow', defaultMessage: 'Tomorrow'})}, - {id: 'custom', label: defineMessage({id: 'status_dropdown.dnd_sub_menu_item.custom', defaultMessage: 'Choose date and time'})}, - ]; - static defaultProps = { - userId: '', - profilePicture: '', - }; - - constructor(props: Props) { - super(props); - - this.state = { - openUp: false, - width: 0, - isStatusSet: false, - }; - } - - openProfileModal = (): void => { - this.props.actions.openModal({ - modalId: ModalIdentifiers.USER_SETTINGS, - dialogType: UserSettingsModal, - dialogProps: {isContentProductSettings: false}, - }); - }; - - setStatus = (status: string, dndEndTime?: number): void => { - this.props.actions.setStatus({ - user_id: this.props.userId, - status, - dnd_end_time: dndEndTime, - }); - }; - - isUserOutOfOffice = (): boolean => { - return this.props.status === UserStatuses.OUT_OF_OFFICE; - }; - - setOnline = (event: Event): void => { - event.preventDefault(); - this.setStatus(UserStatuses.ONLINE); - }; - - setOffline = (event: Event): void => { - event.preventDefault(); - this.setStatus(UserStatuses.OFFLINE); - }; - - setAway = (event: Event): void => { - event.preventDefault(); - this.setStatus(UserStatuses.AWAY); - }; - - setDnd = (index: number): void => { - const currentDate = getCurrentMomentForTimezone(this.props.timezone); - let endTime = currentDate; - switch (index) { - case 0: - endTime = moment(0); - break; - case 1: - // add 30 minutes in current time - endTime = currentDate.add(30, 'minutes'); - break; - case 2: - // add 1 hour in current time - endTime = currentDate.add(1, 'hour'); - break; - case 3: - // add 2 hours in current time - endTime = currentDate.add(2, 'hours'); - break; - case 4: - // set to next day 9 in the morning - endTime = currentDate.add(1, 'day').set({hour: 9, minute: 0}); - break; - } - - this.setStatus(UserStatuses.DND, endTime.utc().unix()); - }; - - setCustomTimedDnd = (): void => { - const dndCustomTimePicker = { - modalId: ModalIdentifiers.DND_CUSTOM_TIME_PICKER, - dialogType: DndCustomTimePicker, - dialogProps: { - currentDate: this.props.timezone ? getCurrentDateTimeForTimezone(this.props.timezone) : new Date(), - }, - }; - - this.props.actions.openModal(dndCustomTimePicker); - }; - - showStatusChangeConfirmation = (status: string): void => { - const resetStatusModalData = { - modalId: ModalIdentifiers.RESET_STATUS, - dialogType: ResetStatusModal, - dialogProps: {newStatus: status}, - }; - - this.props.actions.openModal(resetStatusModalData); - }; - - renderProfilePicture = (size: TAvatarSizeToken): ReactNode => { - if (!this.props.profilePicture) { - return null; - } - return ( - - ); - }; - - handleClearStatus = (e: React.MouseEvent | React.MouseEvent | React.TouchEvent): void => { - e.stopPropagation(); - e.preventDefault(); - this.props.actions.unsetCustomStatus(); - }; - - handleEmitUserLoggedOutEvent = (): void => { - GlobalActions.emitUserLoggedOutEvent(); - }; - - onToggle = (open: boolean): void => { - this.props.actions.setStatusDropdown(open); - }; - - handleCompleteYourProfileTask = (): void => { - const taskName = OnboardingTasksName.COMPLETE_YOUR_PROFILE; - const steps = TaskNameMapToSteps[taskName]; - const currentUserId = this.props.currentUser.id; - const preferences = [ - { - user_id: currentUserId, - category: OnboardingTaskCategory, - name: taskName, - value: steps.FINISHED.toString(), - }, - ]; - this.props.actions.savePreferences(currentUserId, preferences); - }; - - handleCustomStatusEmojiClick = (event: React.MouseEvent): void => { - event.stopPropagation(); - const customStatusInputModalData = { - modalId: ModalIdentifiers.CUSTOM_STATUS, - dialogType: CustomStatusModal, - }; - this.props.actions.openModal(customStatusInputModalData); - }; - - renderCustomStatus = (isStatusSet: boolean | undefined): ReactNode => { - if (!this.props.isCustomStatusEnabled) { - return null; - } - const {customStatus} = this.props; - - let customStatusText; - let customStatusHelpText; - switch (true) { - case isStatusSet && customStatus?.text && customStatus.text.length > 0: - customStatusText = customStatus?.text; - break; - case isStatusSet && !customStatus?.text && customStatus?.duration === CustomStatusDuration.DONT_CLEAR: - customStatusHelpText = this.props.intl.formatMessage({id: 'status_dropdown.set_custom_text', defaultMessage: 'Set custom status text...'}); - break; - case isStatusSet && !customStatus?.text && customStatus?.duration !== CustomStatusDuration.DONT_CLEAR: - customStatusText = ''; - break; - case !isStatusSet: - customStatusHelpText = this.props.intl.formatMessage({id: 'status_dropdown.set_custom', defaultMessage: 'Set a custom status'}); - } - - const customStatusEmoji = isStatusSet ? ( - - - - ) : ( - - ); - - const pulsatingDot = !isStatusSet && this.props.showCustomStatusPulsatingDot && ( - - ); - - const clearableTooltipText = ( - - ); - - const clearButton = isStatusSet && !pulsatingDot && ( -

- - - -
- ); - - const expiryTime = isStatusSet && customStatus?.expires_at && customStatus.duration !== CustomStatusDuration.DONT_CLEAR && - ( - 0, - })} - withinBrackets={true} - /> - ); - - return ( - - - - - {customStatusEmoji} - - - - {customStatusHelpText} - - {clearButton} - {pulsatingDot} - - {expiryTime} - - - ); - }; - - renderDndExtraText = (dndEndTime?: number, timezone?: string) => { - if (!(dndEndTime && dndEndTime > 0)) { - return this.props.intl.formatMessage({id: 'status_dropdown.set_dnd.extra', defaultMessage: 'Disables all notifications'}); - } - - const tz = timezone || getBrowserTimezone(); - const currentTime = moment().tz(tz); - const endTime = moment.unix(dndEndTime).tz(tz); - - let formattedEndTime; - - const diffDays = endTime.clone().startOf('day').diff(currentTime.clone().startOf('day'), 'days'); - - switch (diffDays) { - case 0: - formattedEndTime = ( - - ); - break; - case 1: - formattedEndTime = ( - - ); - break; - default: - formattedEndTime = ( - - ); - } - - return formattedEndTime; - }; - - render = (): JSX.Element => { - const {intl} = this.props; - const needsConfirm = this.isUserOutOfOffice() && this.props.autoResetPref === ''; - const {status, customStatus, isCustomStatusExpired, currentUser, timezone, dndEndTime} = this.props; - const isStatusSet = customStatus && !isCustomStatusExpired && (customStatus.text?.length > 0 || customStatus.emoji?.length > 0); - - const setOnline = needsConfirm ? () => this.showStatusChangeConfirmation('online') : this.setOnline; - const setDnd = needsConfirm ? () => this.showStatusChangeConfirmation('dnd') : this.setDnd; - const setAway = needsConfirm ? () => this.showStatusChangeConfirmation('away') : this.setAway; - const setOffline = needsConfirm ? () => this.showStatusChangeConfirmation('offline') : this.setOffline; - const setCustomTimedDnd = needsConfirm ? () => this.showStatusChangeConfirmation('dnd') : this.setCustomTimedDnd; - - const selectedIndicator = ( - - ); - - const dndSubMenuItems = ([ - { - id: 'dndSubMenu-header', - direction: 'right', - text: this.props.intl.formatMessage({id: 'status_dropdown.dnd_sub_menu_header', defaultMessage: 'Clear after:'}), - isHeader: true, - }, - ] as MenuType[])?.concat( - this.dndTimes.map(({id, label}, index) => { - let text: MenuType['text'] = this.props.intl.formatMessage(label); - if (index === 4) { - const tomorrow = getCurrentMomentForTimezone(this.props.timezone).add(1, 'day').set({hour: 9, minute: 0}).toDate(); - text = ( - <> - {text} - - - {', '} - - - - ); - } - return { - id: `dndTime-${id}`, - direction: 'right', - text, - action: - index === 5 ? () => setCustomTimedDnd() : () => setDnd(index), - }; - }), - ); - - const customStatusComponent = this.renderCustomStatus(isStatusSet); - - let menuAriaLabeltext; - switch (this.props.status) { - case UserStatuses.AWAY: - menuAriaLabeltext = intl.formatMessage({ - id: 'status_dropdown.profile_button_label.away', - defaultMessage: 'Current status: Away. Select to open profile and status menu.', - }); - break; - case UserStatuses.DND: - menuAriaLabeltext = intl.formatMessage({ - id: 'status_dropdown.profile_button_label.dnd', - defaultMessage: 'Current status: Do not disturb. Select to open profile and status menu.', - }); - break; - case UserStatuses.OFFLINE: - menuAriaLabeltext = intl.formatMessage({ - id: 'status_dropdown.profile_button_label.offline', - defaultMessage: 'Current status: Offline. Select to open profile and status menu.', - }); - break; - case UserStatuses.ONLINE: - menuAriaLabeltext = intl.formatMessage({ - id: 'status_dropdown.profile_button_label.online', - defaultMessage: 'Current status: Online. Select to open profile and status menu.', - }); - break; - case UserStatuses.OUT_OF_OFFICE: - menuAriaLabeltext = intl.formatMessage({ - id: 'status_dropdown.profile_button_label.ooo', - defaultMessage: 'Current status: Out of office. Select to open profile and status menu.', - }); - break; - default: - menuAriaLabeltext = intl.formatMessage({ - id: 'status_dropdown.profile_button_label', - defaultMessage: 'Select to open profile and status menu.', - }); - } - - const dndExtraText = this.renderDndExtraText(dndEndTime, timezone); - - return ( - - - - {currentUser && ( - - {this.renderProfilePicture('lg')} -
- {`${currentUser.first_name} ${currentUser.last_name}`} - - {'@' + currentUser.username} - -
-
- )} - - - )} - extraText={this.props.intl.formatMessage(statusDropdownMessages.ooo.extra)} - rightDecorator={selectedIndicator} - /> - - {customStatusComponent} - - - )} - rightDecorator={status === 'online' && selectedIndicator} - id={'status-menu-online'} - /> - - )} - rightDecorator={status === 'away' && selectedIndicator} - id={'status-menu-away'} - /> - - )} - rightDecorator={status === 'dnd' && selectedIndicator} - direction={'left'} - openUp={this.state.openUp} - id={'status-menu-dnd'} - action={() => setDnd(0)} - /> - - )} - rightDecorator={status === 'offline' && selectedIndicator} - id={'status-menu-offline'} - /> - - - - } - > - {this.props.showCompleteYourProfileTour && ( -
- -
- )} -
-
- - - - } - /> - - -
-
- ); - }; -} -export default injectIntl(StatusDropdown); diff --git a/webapp/channels/src/components/user_account_menu/__snapshots__/user_account_name_menuitem.test.tsx.snap b/webapp/channels/src/components/user_account_menu/__snapshots__/user_account_name_menuitem.test.tsx.snap new file mode 100644 index 0000000000..503dd88dda --- /dev/null +++ b/webapp/channels/src/components/user_account_menu/__snapshots__/user_account_name_menuitem.test.tsx.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UserAccountNameMenuItem should not break if no props are passed 1`] = ` +
+ +
+`; diff --git a/webapp/channels/src/components/user_account_menu/index.ts b/webapp/channels/src/components/user_account_menu/index.ts new file mode 100644 index 0000000000..08f321ae66 --- /dev/null +++ b/webapp/channels/src/components/user_account_menu/index.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {ConnectedProps} from 'react-redux'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import type {Dispatch} from 'redux'; + +import {Client4} from 'mattermost-redux/client'; +import {Preferences} from 'mattermost-redux/constants'; +import {get} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; +import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users'; + +import {openModal} from 'actions/views/modals'; +import {makeGetCustomStatus, isCustomStatusExpired, isCustomStatusEnabled} from 'selectors/views/custom_status'; + +import type {GlobalState} from 'types/store'; + +import UserAccountMenu from './user_account_menu'; + +function makeMapStateToProps() { + const getCustomStatus = makeGetCustomStatus(); + + return function mapStateToProps(state: GlobalState) { + const currentUser = getCurrentUser(state); + const userId = currentUser?.id; + const username = currentUser?.username; + const userFirstName = currentUser?.first_name; + const userLastName = currentUser?.last_name; + const customStatus = getCustomStatus(state, userId); + + return { + userId, + username, + userFirstName, + userLastName, + profilePicture: Client4.getProfilePictureUrl(userId, currentUser?.last_picture_update), + autoResetPref: get(state, Preferences.CATEGORY_AUTO_RESET_MANUAL_STATUS, userId, ''), + status: getStatusForUserId(state, userId), + customStatus, + isCustomStatusExpired: isCustomStatusExpired(state, customStatus), + isCustomStatusEnabled: isCustomStatusEnabled(state), + timezone: getCurrentTimezone(state), + }; + }; +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + actions: bindActionCreators({ + openModal, + }, dispatch), + }; +} + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +export type PropsFromRedux = ConnectedProps; + +export default connector(UserAccountMenu); diff --git a/webapp/channels/src/components/user_account_menu/user_account_away_menuitem.tsx b/webapp/channels/src/components/user_account_menu/user_account_away_menuitem.tsx new file mode 100644 index 0000000000..0dab0ad7ba --- /dev/null +++ b/webapp/channels/src/components/user_account_menu/user_account_away_menuitem.tsx @@ -0,0 +1,80 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useMemo} from 'react'; +import {FormattedMessage} from 'react-intl'; +import {useDispatch} from 'react-redux'; + +import {CheckIcon, ClockIcon} from '@mattermost/compass-icons/components'; +import type {UserProfile} from '@mattermost/types/users'; + +import {setStatus} from 'mattermost-redux/actions/users'; + +import {openModal} from 'actions/views/modals'; + +import * as Menu from 'components/menu'; +import ResetStatusModal from 'components/reset_status_modal'; + +import {ModalIdentifiers, UserStatuses} from 'utils/constants'; + +interface Props { + userId: UserProfile['id']; + shouldConfirmBeforeStatusChange: boolean; + isStatusAway: boolean; +} + +export default function UserAccountAwayMenuItem(props: Props) { + const dispatch = useDispatch(); + + function handleClick() { + if (props.shouldConfirmBeforeStatusChange) { + dispatch(openModal({ + modalId: ModalIdentifiers.RESET_STATUS, + dialogType: ResetStatusModal, + dialogProps: { + newStatus: UserStatuses.AWAY, + }, + })); + } else { + dispatch(setStatus({ + user_id: props.userId, + status: UserStatuses.AWAY, + })); + } + } + + const trailingElement = useMemo(() => { + if (props.isStatusAway) { + return ( +