[MM-55274] User account menu doesn't follow standard accessible keyboard behavior (#29173)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
3f9aa4a212
Коммит
4b6216f5a4
@@ -26,9 +26,9 @@ describe('Verify Accessibility Support in Different Images', () => {
|
|||||||
|
|
||||||
it('MM-T1508 Accessibility support in different images', () => {
|
it('MM-T1508 Accessibility support in different images', () => {
|
||||||
// * Verify image alt in profile image
|
// * Verify image alt in profile image
|
||||||
cy.uiGetProfileHeader().
|
cy.get('#userAccountMenuButton').within(() => {
|
||||||
find('.Avatar').
|
cy.findByAltText('user profile image').should('be.visible');
|
||||||
should('have.attr', 'alt', 'user profile image');
|
});
|
||||||
|
|
||||||
// # Upload an image in the post
|
// # Upload an image in the post
|
||||||
cy.get('#fileUploadInput').attachFile('small-image.png');
|
cy.get('#fileUploadInput').attachFile('small-image.png');
|
||||||
|
|||||||
@@ -163,9 +163,6 @@ describe('Profile > Profile Settings > Email', () => {
|
|||||||
// # Click on the link
|
// # Click on the link
|
||||||
cy.visit(permalink);
|
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.
|
// # Wait for one second for the mail to be sent out.
|
||||||
cy.wait(TIMEOUTS.FIVE_SEC);
|
cy.wait(TIMEOUTS.FIVE_SEC);
|
||||||
|
|
||||||
@@ -199,7 +196,7 @@ describe('Profile > Profile Settings > Email', () => {
|
|||||||
|
|
||||||
// # Close modal then logout
|
// # Close modal then logout
|
||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
cy.uiOpenUserMenu('Log Out');
|
cy.uiLogout();
|
||||||
|
|
||||||
// # Wait for one second for the mail to be sent out.
|
// # Wait for one second for the mail to be sent out.
|
||||||
cy.wait(TIMEOUTS.ONE_SEC);
|
cy.wait(TIMEOUTS.ONE_SEC);
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ describe('Settings > Sidebar > General > Edit', () => {
|
|||||||
// # Step 3
|
// # Step 3
|
||||||
// * Verify that we've logged in as the temp user
|
// * Verify that we've logged in as the temp user
|
||||||
cy.visit(offTopicUrl);
|
cy.visit(offTopicUrl);
|
||||||
cy.uiOpenUserMenu().findByText(`@${newTempUserName}`);
|
cy.uiOpenUserMenu().findByText(`@${newTempUserName}`).should('exist');
|
||||||
cy.uiGetSetStatusButton().click();
|
cy.get('body').type('{esc}');
|
||||||
|
|
||||||
// # Step 4
|
// # Step 4
|
||||||
const text = `${newTempUserName} test message!`;
|
const text = `${newTempUserName} test message!`;
|
||||||
|
|||||||
@@ -124,5 +124,5 @@ function verifyUsername(username) {
|
|||||||
cy.uiOpenUserMenu().findByText(`@${username}`);
|
cy.uiOpenUserMenu().findByText(`@${username}`);
|
||||||
|
|
||||||
// # Close the user menu
|
// # Close the user menu
|
||||||
cy.uiGetSetStatusButton().click();
|
cy.get('body').type('{esc}');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import * as TIMEOUTS from '../../../fixtures/timeouts';
|
||||||
|
|
||||||
import {clickCategoryMenuItem} from './helpers';
|
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', () => {
|
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');
|
clickCategoryMenuItem('CHANNELS', 'Create New Category');
|
||||||
|
|
||||||
|
cy.wait(TIMEOUTS.ONE_SEC);
|
||||||
|
|
||||||
cy.get('body').type('{esc}', {force: true});
|
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
|
// # Verify that Create New Category exists on Favorites category and click on it
|
||||||
clickCategoryMenuItem('FAVORITES', 'Create New Category');
|
clickCategoryMenuItem('FAVORITES', 'Create New Category');
|
||||||
|
|
||||||
|
cy.wait(TIMEOUTS.ONE_SEC);
|
||||||
|
|
||||||
cy.get('body').type('{esc}', {force: true});
|
cy.get('body').type('{esc}', {force: true});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,15 +28,23 @@ describe('Custom Status - Setting a Custom Status', () => {
|
|||||||
|
|
||||||
it('MM-T3836_1 should open status dropdown', () => {
|
it('MM-T3836_1 should open status dropdown', () => {
|
||||||
// # Click on the sidebar header to 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
|
// * 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', () => {
|
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
|
// # Open custom status modal
|
||||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
|
||||||
cy.get('#custom_status_modal').should('exist');
|
cy.get('#custom_status_modal').should('exist');
|
||||||
|
|
||||||
// * Check if all the default suggestions exist
|
// * Check if all the default suggestions exist
|
||||||
@@ -111,13 +119,15 @@ describe('Custom Status - Setting a Custom Status', () => {
|
|||||||
trigger('mouseover');
|
trigger('mouseover');
|
||||||
|
|
||||||
// * Custom status tooltip should be visible
|
// * 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').within(() => {
|
||||||
cy.get('#custom-status-tooltip .custom-status span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji);
|
// * 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
|
// * Tooltip should contain the correct custom status text
|
||||||
cy.get('#custom-status-tooltip .custom-status span.custom-status-text').should('have.text', customStatus.text);
|
cy.findByText(customStatus.text).should('exist');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('MM-T3836_8 should open custom status modal when emoji in LHS header is clicked', () => {
|
it('MM-T3836_8 should open custom status modal when emoji in LHS header is clicked', () => {
|
||||||
|
|||||||
@@ -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', () => {
|
it('MM-T3846_1 should change the emoji to speech balloon when typed in the input', () => {
|
||||||
// # Open the custom status modal
|
// # 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
|
// * Default emoji is currently visible in the custom status input
|
||||||
cy.get('#custom_status_modal .StatusModal__emoji-button span').should('have.class', 'icon--emoji');
|
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().
|
cy.uiGetProfileHeader().
|
||||||
find('.emoticon').
|
find('.emoticon').
|
||||||
should('have.attr', 'data-emoticon', customStatus.emoji);
|
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', () => {
|
it('MM-T3846_5 should show custom status with emoji in the status dropdown', () => {
|
||||||
// # Open user menu
|
// # 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('body').type('{esc}');
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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
|
// # 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
|
// * Custom status modal should open
|
||||||
cy.get('#custom_status_modal').should('exist');
|
cy.get('#custom_status_modal').should('exist');
|
||||||
@@ -118,9 +124,9 @@ describe('Custom Status - Setting Your Own Custom Status', () => {
|
|||||||
cy.findByText('Clear Status').click();
|
cy.findByText('Clear Status').click();
|
||||||
|
|
||||||
// # Open user menu
|
// # Open user menu
|
||||||
cy.uiOpenUserMenu();
|
cy.uiOpenUserMenu().within(() => {
|
||||||
|
// * Custom status text should not be displayed in the status dropdown
|
||||||
// * Custom status text should not be displayed in the status dropdown
|
cy.findByText(customStatus.text).should('not.exist');
|
||||||
cy.get('.status-dropdown-menu .custom_status__row').should('not.have.text', customStatus.text);
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe('Custom Status - Recent Statuses', () => {
|
|||||||
|
|
||||||
it('MM-T3847_1 set a status', () => {
|
it('MM-T3847_1 set a status', () => {
|
||||||
// # Open the custom status modal
|
// # 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
|
// # Type the custom status text in the custom status modal input
|
||||||
cy.get('#custom_status_modal .StatusModal__input input').typeWithForce(customStatus.text);
|
cy.get('#custom_status_modal .StatusModal__input input').typeWithForce(customStatus.text);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ describe('Custom Status - Verifying Where Custom Status Appears', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('MM-T3850_1 set a status', () => {
|
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
|
// # Type the custom status text in the custom status modal input
|
||||||
cy.findByPlaceholderText('Set a status').type(customStatus.text, {force: true});
|
cy.findByPlaceholderText('Set a status').type(customStatus.text, {force: true});
|
||||||
|
|||||||
@@ -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 defaultCustomStatuses = ['In a meeting', 'Out for lunch', 'Out sick', 'Working from home', 'On a vacation'];
|
||||||
const customStatus = {
|
const customStatus = {
|
||||||
emoji: 'hamburger',
|
emoji: 'hamburger',
|
||||||
|
emojiAriaLabel: ':hamburger:',
|
||||||
text: 'Out for lunch',
|
text: 'Out for lunch',
|
||||||
duration: '30 minutes',
|
duration: '30 minutes',
|
||||||
};
|
};
|
||||||
@@ -36,19 +37,25 @@ describe('MM-T4063 Custom status expiry', () => {
|
|||||||
const expiryTimeFormat = 'h:mm A';
|
const expiryTimeFormat = 'h:mm A';
|
||||||
it('MM-T4063_1 should open status dropdown', () => {
|
it('MM-T4063_1 should open status dropdown', () => {
|
||||||
// # Click on the sidebar header to 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
|
// * 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', () => {
|
it('MM-T4063_2 Custom status modal opens with 5 default statuses listed', () => {
|
||||||
// # Open custom status modal
|
// # Open custom status modal
|
||||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
cy.uiOpenUserMenu('Set custom status');
|
||||||
cy.get('#custom_status_modal').should('exist');
|
|
||||||
|
|
||||||
// * Check if all the default suggestions exist
|
cy.findByRole('dialog', {name: 'Set a status'}).should('exist').within(() => {
|
||||||
defaultCustomStatuses.map((statusText) => cy.get('#custom_status_modal .statusSuggestion__content').contains('span', statusText));
|
// * 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', () => {
|
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', () => {
|
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
|
// # Click on the sidebar header to open status dropdown
|
||||||
cy.get('.MenuWrapper .status-wrapper').click();
|
cy.uiGetSetStatusButton().click();
|
||||||
|
|
||||||
// * Check if the status dropdown opens
|
// * 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
|
// * Correct clear time should be displayed in the status dropdown
|
||||||
cy.get('.status-dropdown-menu .custom_status__container').should('have.text', customStatus.text);
|
cy.findByText(expiresAtRegexp).should('exist');
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('MM-T4063_6 custom status should be cleared after duration of set custom status', () => {
|
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', () => {
|
it('MM-T4063_7 current custom status should display expiry time in custom status modal', () => {
|
||||||
// # Open custom status modal
|
// # Open custom status modal
|
||||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
cy.get('.userAccountMenu_customStatusMenuItem').should('be.visible').click();
|
||||||
cy.get('#custom_status_modal').should('exist');
|
|
||||||
|
|
||||||
// * Should show expiry time of status when current status is selected
|
// * Should show expiry time of status when current status is selected
|
||||||
cy.get('#custom_status_modal .statusSuggestion__content').contains('span', customStatus.text).click();
|
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', () => {
|
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
|
// # Forwarding the time by the duration of custom status
|
||||||
cy.clock(Date.now());
|
cy.clock(Date.now());
|
||||||
cy.tick(waitingTime * 60 * 1000);
|
cy.tick(waitingTime * 60 * 1000).then(() => {
|
||||||
|
// # Open custom status modal
|
||||||
// # Open custom status modal
|
cy.uiOpenUserMenu().within(() => {
|
||||||
cy.get('.status-dropdown-menu').click();
|
// * Verify that there is no custom status in the dropdown
|
||||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
cy.findByText('Set custom status').should('exist').and('be.visible');
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,86 +21,30 @@ describe('MM-T4064 Status expiry visibility', () => {
|
|||||||
cy.visit(channelUrl);
|
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
|
const waitingTime = 60; //minutes
|
||||||
let expiresAt = dayjs();
|
let expiresAt = dayjs();
|
||||||
const expiryTimeFormat = 'h:mm A';
|
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
|
it('MM-T4064_6 should show expiry time in the tooltip of custom status emoji in the post header', () => {
|
||||||
cy.get('#statusDropdownMenu').should('exist');
|
// # Open the user account menu
|
||||||
});
|
cy.uiOpenUserMenu('Set custom status');
|
||||||
|
|
||||||
it('MM-T4064_2 Custom status modal opens with 5 default statuses listed', () => {
|
// * Verify that the custom status modal opens
|
||||||
// # Open custom status modal
|
cy.findByRole('dialog', {name: 'Set a status'}).should('exist').within(() => {
|
||||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
// # Select a custom status from the suggestions
|
||||||
cy.get('#custom_status_modal').should('exist');
|
cy.get('.statusSuggestion__row').first().click();
|
||||||
|
|
||||||
// * Check if all the default suggestions exist
|
// # Click on the Set Status button
|
||||||
defaultCustomStatuses.map((statusText) => cy.get('#custom_status_modal .statusSuggestion__content').contains('span', statusText));
|
cy.findByText('Set Status').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
// * Modal should be closed
|
// * Modal should be closed
|
||||||
cy.get('#custom_status_modal').should('not.exist');
|
cy.get('#custom_status_modal').should('not.exist');
|
||||||
|
|
||||||
// # Setting the time at which the custom status should be expired
|
// # 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');
|
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
|
// # Post a message in the channel
|
||||||
cy.postMessage('Hello World!');
|
cy.postMessage('Hello World!');
|
||||||
|
|
||||||
|
|||||||
@@ -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', () => {
|
it('MM-T4065_1 should open status dropdown', () => {
|
||||||
// # Click on the sidebar header to 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
|
// * 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', () => {
|
it('MM-T4065_2 Custom status modal opens with 5 default statuses listed', () => {
|
||||||
// # Open custom status modal
|
// # 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');
|
cy.get('#custom_status_modal').should('exist');
|
||||||
|
|
||||||
// * Check if all the default suggestions exist
|
// * Check if all the default suggestions exist
|
||||||
|
|||||||
@@ -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');
|
const months = dateToBeSelected.get('month') - today.get('month');
|
||||||
it('MM-T4066_1 should open status dropdown', () => {
|
it('MM-T4066_1 should open status dropdown', () => {
|
||||||
// # Click on the sidebar header to 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
|
// * 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', () => {
|
it('MM-T4066_2 Custom status modal opens with 5 default statuses listed', () => {
|
||||||
// # Open custom status modal
|
// # 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');
|
cy.get('#custom_status_modal').should('exist');
|
||||||
|
|
||||||
// * Check if all the default suggestions exist
|
// * Check if all the default suggestions exist
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
|
|
||||||
describe('Integrations', () => {
|
describe('Integrations', () => {
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{command: '/away', className: 'icon-clock', message: 'You are now away'},
|
{command: '/away', className: 'userAccountMenu_awayMenuItem_icon', 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: '/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: 'icon-circle-outline', message: 'You are now offline'},
|
{command: '/offline', className: 'userAccountMenu_offlineMenuItem_icon', message: 'You are now offline'},
|
||||||
{command: '/online', className: 'icon-check-circle', message: 'You are now online'},
|
{command: '/online', className: 'userAccountMenu_onlineMenuItem_icon', message: 'You are now online'},
|
||||||
];
|
];
|
||||||
|
|
||||||
let offTopicUrl;
|
let offTopicUrl;
|
||||||
@@ -67,7 +67,7 @@ describe('Integrations', () => {
|
|||||||
function verifyUserStatus(testCase, isCompactMode) {
|
function verifyUserStatus(testCase, isCompactMode) {
|
||||||
// * Verify that the user status is as indicated
|
// * Verify that the user status is as indicated
|
||||||
cy.uiGetProfileHeader().
|
cy.uiGetProfileHeader().
|
||||||
find('i').
|
find('svg').
|
||||||
should('be.visible').
|
should('be.visible').
|
||||||
and('have.class', testCase.className);
|
and('have.class', testCase.className);
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ describe('Integrations', () => {
|
|||||||
|
|
||||||
it('MM-T670 /away', () => {
|
it('MM-T670 /away', () => {
|
||||||
// # Set online status and verify it's changed as the initial status
|
// # Set online status and verify it's changed as the initial status
|
||||||
setStatus(online.name, online.profileClassName);
|
setStatus(online.name);
|
||||||
|
|
||||||
verifyUserStatus(away);
|
verifyUserStatus(away);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('MM-T672 /offline', () => {
|
it('MM-T672 /offline', () => {
|
||||||
// # Set online status and verify it's changed as the initial status
|
// # Set online status and verify it's changed as the initial status
|
||||||
setStatus(online.name, online.profileClassName);
|
setStatus(online.name);
|
||||||
|
|
||||||
verifyUserStatus(offline);
|
verifyUserStatus(offline);
|
||||||
|
|
||||||
@@ -60,17 +60,14 @@ describe('Integrations', () => {
|
|||||||
|
|
||||||
it('MM-T674 /online', () => {
|
it('MM-T674 /online', () => {
|
||||||
// # Set offline status and verify it's changed as the initial status
|
// # Set offline status and verify it's changed as the initial status
|
||||||
setStatus(offline.name, offline.className);
|
setStatus(offline.name);
|
||||||
|
|
||||||
verifyUserStatus(online);
|
verifyUserStatus(online);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function setStatus(status, icon) {
|
function setStatus(status) {
|
||||||
cy.apiUpdateUserStatus(status);
|
cy.apiUpdateUserStatus(status);
|
||||||
cy.uiGetProfileHeader().
|
|
||||||
find('i').
|
|
||||||
and('have.class', icon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyUserStatus(testCase) {
|
function verifyUserStatus(testCase) {
|
||||||
@@ -90,9 +87,7 @@ function verifyUserStatus(testCase) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// * Verify status shown at user profile in LHS
|
// * Verify status shown at user profile in LHS
|
||||||
cy.uiGetProfileHeader().
|
cy.get('#userAccountMenuButtonDescribedBy').should('exist').and('include.text', `Status is "${testCase.name.charAt(0).toUpperCase() + testCase.name.slice(1)}"`);
|
||||||
find('i').
|
|
||||||
and('have.class', testCase.profileClassName || testCase.className);
|
|
||||||
|
|
||||||
// # Post a message
|
// # Post a message
|
||||||
cy.postMessage(testCase.name);
|
cy.postMessage(testCase.name);
|
||||||
|
|||||||
@@ -33,10 +33,9 @@ describe('Main menu', () => {
|
|||||||
cy.apiLogin(testUser);
|
cy.apiLogin(testUser);
|
||||||
cy.visit(`/${testTeam.name}/channels/town-square`);
|
cy.visit(`/${testTeam.name}/channels/town-square`);
|
||||||
|
|
||||||
cy.uiOpenUserMenu();
|
cy.uiOpenUserMenu().within(() => {
|
||||||
|
cy.findAllByRole('separator').last().click();
|
||||||
cy.findByRole('menu').should('exist').find('.menu-divider:visible').last().click();
|
});
|
||||||
cy.findByRole('menu').should('exist');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show integrations option for system administrator', () => {
|
it('should show integrations option for system administrator', () => {
|
||||||
|
|||||||
@@ -10,15 +10,14 @@
|
|||||||
// Stage: @prod
|
// Stage: @prod
|
||||||
// Group: @channels @menu @custom_status @status_menu
|
// Group: @channels @menu @custom_status @status_menu
|
||||||
|
|
||||||
import * as TIMEOUTS from '../../../fixtures/timeouts';
|
|
||||||
import theme from '../../../fixtures/theme.json';
|
import theme from '../../../fixtures/theme.json';
|
||||||
|
|
||||||
describe('Status dropdown menu', () => {
|
describe('Status dropdown menu', () => {
|
||||||
const statusTestCases = [
|
const statusTestCases = [
|
||||||
{text: 'Online', className: 'icon-check-circle', profileClassName: 'icon-check-circle'},
|
{text: 'Online', className: 'userAccountMenu_onlineMenuItem_icon'},
|
||||||
{text: 'Away', className: 'icon-clock'},
|
{text: 'Away', className: 'userAccountMenu_awayMenuItem_icon'},
|
||||||
{text: 'Do not disturb', className: 'icon-minus-circle'},
|
{text: 'Do not disturb', className: 'userAccountMenu_dndMenuItem_icon'},
|
||||||
{text: 'Offline', className: 'icon-circle-outline'},
|
{text: 'Offline', className: 'userAccountMenu_offlineMenuItem_icon'},
|
||||||
];
|
];
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
@@ -40,9 +39,10 @@ describe('Status dropdown menu', () => {
|
|||||||
|
|
||||||
// * Verify all available statuses are shown with icon and text
|
// * Verify all available statuses are shown with icon and text
|
||||||
statusTestCases.forEach((tc) => {
|
statusTestCases.forEach((tc) => {
|
||||||
cy.get('@userMenu').findByText(tc.text).
|
cy.uiGetStatusMenu().within(() => {
|
||||||
parent().
|
cy.findByText(tc.text).should('be.visible');
|
||||||
find('i').should('have.class', tc.className);
|
cy.get('svg').filter(`.${tc.className}`).should('have.length', 1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ describe('Status dropdown menu', () => {
|
|||||||
cy.uiOpenUserMenu().as('userMenu');
|
cy.uiOpenUserMenu().as('userMenu');
|
||||||
|
|
||||||
// * Verify "Set a Custom Status" header is clickable
|
// * 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', () => {
|
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();
|
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', () => {
|
it('MM-T4914 Profile menu header is clickable, opens Profile settings', () => {
|
||||||
// # Open user menu
|
// # Open user menu
|
||||||
cy.uiOpenUserMenu().as('userMenu');
|
cy.uiOpenUserMenu().within(() => {
|
||||||
|
// * Verify menu header is clickable
|
||||||
// * Verify menu header is clickable
|
cy.get('li').first().should('have.css', 'cursor', 'pointer').click();
|
||||||
cy.get('@userMenu').get('.MenuHeader').should('have.css', 'cursor', 'pointer').click();
|
});
|
||||||
|
|
||||||
// * Verify click on header opens Profile settings modal
|
// * Verify click on header opens Profile settings modal
|
||||||
cy.findByRole('dialog', {name: 'Profile'}).should('be.visible');
|
cy.findByRole('dialog', {name: 'Profile'}).should('be.visible');
|
||||||
@@ -111,14 +101,14 @@ function stepThroughStatuses(statusTestCases = []) {
|
|||||||
statusTestCases.forEach((tc) => {
|
statusTestCases.forEach((tc) => {
|
||||||
// # Open user menu and click option
|
// # Open user menu and click option
|
||||||
if (tc.text === 'Do not disturb') {
|
if (tc.text === 'Do not disturb') {
|
||||||
cy.uiOpenDndStatusSubMenu().find('#dndTime-thirty_minutes_menuitem').click();
|
cy.uiOpenDndStatusSubMenuAndClick30Mins();
|
||||||
} else {
|
} else {
|
||||||
cy.uiOpenUserMenu(tc.text);
|
cy.uiOpenUserMenu(tc.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// # Verify correct status icon is shown on user's profile picture
|
// # Verify correct status icon is shown on user's profile picture
|
||||||
cy.uiGetProfileHeader().
|
cy.uiGetSetStatusButton().within(() => {
|
||||||
find('i').
|
cy.get('svg').filter(`.${tc.className}`).should('have.length', 1);
|
||||||
and('have.class', tc.profileClassName || tc.className);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ describe('Notifications', () => {
|
|||||||
cy.uiOpenUserMenu().findByText(`@${firstUser.username}`);
|
cy.uiOpenUserMenu().findByText(`@${firstUser.username}`);
|
||||||
|
|
||||||
// # Close the user menu
|
// # Close the user menu
|
||||||
cy.uiGetSetStatusButton().click();
|
cy.get('body').type('{esc}');
|
||||||
|
|
||||||
// * Check that 'Town Square' is currently being selected
|
// * Check that 'Town Square' is currently being selected
|
||||||
cy.get('.active').within(() => {
|
cy.get('.active').within(() => {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ describe('Profile Settings', () => {
|
|||||||
fileTypes.forEach((fileType, index) => {
|
fileTypes.forEach((fileType, index) => {
|
||||||
it(`MM-T2078_${index + 1} Profile picture: file ${fileType.extension} type accepted`, () => {
|
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
|
// # 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');
|
cy.uiOpenProfileModal('Profile Settings');
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ describe('Profile Settings', () => {
|
|||||||
cy.uiClose();
|
cy.uiClose();
|
||||||
|
|
||||||
// # Save the new custom profile picture link so it can be compared to the old one
|
// # 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() {
|
cy.then(function() {
|
||||||
expect(this.customProfilePictureLink).to.not.equal(this.defaultProfilePictureLink);
|
expect(this.customProfilePictureLink).to.not.equal(this.defaultProfilePictureLink);
|
||||||
|
|||||||
@@ -92,18 +92,18 @@ describe('Custom Theme - Sidebar Styles', () => {
|
|||||||
cy.uiOpenUserMenu('Online');
|
cy.uiOpenUserMenu('Online');
|
||||||
|
|
||||||
// * Check Online Indicator color
|
// * 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
|
// # Set user status to away
|
||||||
cy.uiOpenUserMenu('Away');
|
cy.uiOpenUserMenu('Away');
|
||||||
|
|
||||||
// * Check Away Indicator color
|
// * 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
|
// # Set user status to do not disturb
|
||||||
cy.uiOpenDndStatusSubMenu().find('#dndTime-thirty_minutes_menuitem').click();
|
cy.uiOpenDndStatusSubMenuAndClick30Mins();
|
||||||
|
|
||||||
// * Check Do Not Disturb Indicator color
|
// * 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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe('SignIn Authentication', () => {
|
|||||||
cy.url().should('include', '/channels/town-square');
|
cy.url().should('include', '/channels/town-square');
|
||||||
|
|
||||||
// # Click logout via user menu
|
// # 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
|
// * Check that it logout successfully and it redirects into the login page
|
||||||
cy.url().should('include', '/login');
|
cy.url().should('include', '/login');
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ describe('Login page with open server', () => {
|
|||||||
cy.url().should('include', '/channels/town-square');
|
cy.url().should('include', '/channels/town-square');
|
||||||
|
|
||||||
// # Click logout via user menu
|
// # 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
|
// * Check that it logout successfully and it redirects into the login page
|
||||||
cy.url().should('include', '/login');
|
cy.url().should('include', '/login');
|
||||||
@@ -209,7 +209,7 @@ describe('Login page with open server', () => {
|
|||||||
cy.url().should('include', '/channels/town-square');
|
cy.url().should('include', '/channels/town-square');
|
||||||
|
|
||||||
// # Click logout via user menu
|
// # 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
|
// * Check that it logout successfully and it redirects into the login page
|
||||||
cy.url().should('include', '/login');
|
cy.url().should('include', '/login');
|
||||||
|
|||||||
@@ -92,12 +92,12 @@ describe('Status of current user', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function verifyStatus(status: 'online' | 'away' | 'offline' | 'dnd') {
|
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');
|
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');
|
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');
|
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');
|
should(status === 'dnd' ? 'exist' : 'not.exist');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Cypress.Commands.add('checkLeftSideBar', (settings = {}) => {
|
|||||||
cy.uiOpenUserMenu().findByText(`@${settings.user.username}`);
|
cy.uiOpenUserMenu().findByText(`@${settings.user.username}`);
|
||||||
|
|
||||||
// # Close status menu
|
// # Close status menu
|
||||||
cy.uiGetSetStatusButton().click();
|
cy.get('body').type('{esc}');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.user.userType === 'Admin' || settings.user.isAdmin) {
|
if (settings.user.userType === 'Admin' || settings.user.isAdmin) {
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ Cypress.Commands.add('uiOpenProductMenu', (item = '') => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('uiGetSetStatusButton', () => {
|
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', () => {
|
Cypress.Commands.add('uiGetProfileHeader', () => {
|
||||||
@@ -36,22 +37,18 @@ Cypress.Commands.add('uiGetProfileHeader', () => {
|
|||||||
|
|
||||||
Cypress.Commands.add('uiGetStatusMenuContainer', (options = {exist: true}) => {
|
Cypress.Commands.add('uiGetStatusMenuContainer', (options = {exist: true}) => {
|
||||||
if (options.exist) {
|
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}) => {
|
Cypress.Commands.add('uiGetStatusMenu', (options = {visible: true}) => {
|
||||||
if (options.visible) {
|
if (options.visible) {
|
||||||
return cy.uiGetStatusMenuContainer().
|
return cy.get('#userAccountMenu').should('exist').and('be.visible');
|
||||||
find('ul').
|
|
||||||
should('be.visible');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cy.uiGetStatusMenuContainer().
|
return cy.get('#userAccountMenu').should('not.exist');
|
||||||
find('ul').
|
|
||||||
should('not.be.visible');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('uiOpenHelpMenu', (item = '') => {
|
Cypress.Commands.add('uiOpenHelpMenu', (item = '') => {
|
||||||
@@ -147,7 +144,7 @@ Cypress.Commands.add('uiOpenSettingsModal', (section = '') => {
|
|||||||
|
|
||||||
Cypress.Commands.add('uiLogout', () => {
|
Cypress.Commands.add('uiLogout', () => {
|
||||||
// # Click logout via user menu
|
// # Click logout via user menu
|
||||||
cy.uiOpenUserMenu('Log Out');
|
cy.uiOpenUserMenu('Log out');
|
||||||
|
|
||||||
cy.url().should('include', '/login');
|
cy.url().should('include', '/login');
|
||||||
cy.get('.login-body-message').should('be.visible');
|
cy.get('.login-body-message').should('be.visible');
|
||||||
|
|||||||
@@ -33,14 +33,8 @@ Cypress.Commands.add('uiGetSystemConsoleMainMenu', () => {
|
|||||||
return getMenu(SYSTEM_CONSOLE_MAIN_MENU);
|
return getMenu(SYSTEM_CONSOLE_MAIN_MENU);
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('uiOpenDndStatusSubMenu', () => {
|
Cypress.Commands.add('uiOpenDndStatusSubMenuAndClick30Mins', () => {
|
||||||
cy.uiOpenUserMenu();
|
cy.uiOpenUserMenu().findByText('Do not disturb').should('be.visible').trigger('mouseover');
|
||||||
|
|
||||||
// # Wait for status menu to transition in
|
cy.findByText('30 mins').should('be.visible').click({force: true});
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {memo, useMemo, useRef} from 'react';
|
import React, {memo, useMemo, useRef} from 'react';
|
||||||
|
import type {MouseEvent, KeyboardEvent} from 'react';
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
import {CustomStatusDuration} from '@mattermost/types/users';
|
import {CustomStatusDuration} from '@mattermost/types/users';
|
||||||
@@ -23,7 +24,7 @@ interface Props {
|
|||||||
spanStyle?: React.CSSProperties;
|
spanStyle?: React.CSSProperties;
|
||||||
emojiStyle?: React.CSSProperties;
|
emojiStyle?: React.CSSProperties;
|
||||||
userID?: string;
|
userID?: string;
|
||||||
onClick?: () => void;
|
onClick?: (event: MouseEvent<HTMLSpanElement> | KeyboardEvent<HTMLSpanElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomStatusEmoji({
|
function CustomStatusEmoji({
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ const ExpiryTime = ({time, timezone, className, showPrefix, withinBrackets}: Pro
|
|||||||
id='custom_status.expiry.until'
|
id='custom_status.expiry.until'
|
||||||
defaultMessage='Until {time}'
|
defaultMessage='Until {time}'
|
||||||
values={{time: ''}}
|
values={{time: ''}}
|
||||||
/>{' '}
|
/>
|
||||||
|
{' '}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import type {MouseEvent, KeyboardEvent} from 'react';
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
import {getEmojiImageUrl} from 'mattermost-redux/utils/emoji_utils';
|
import {getEmojiImageUrl} from 'mattermost-redux/utils/emoji_utils';
|
||||||
@@ -14,7 +15,7 @@ interface ComponentProps {
|
|||||||
emojiName: string;
|
emojiName: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
emojiStyle?: React.CSSProperties;
|
emojiStyle?: React.CSSProperties;
|
||||||
onClick?: () => void;
|
onClick?: (event: MouseEvent<HTMLSpanElement> | KeyboardEvent<HTMLSpanElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RenderEmoji = ({emojiName, emojiStyle, size, onClick}: ComponentProps) => {
|
const RenderEmoji = ({emojiName, emojiStyle, size, onClick}: ComponentProps) => {
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import type {ProductIdentifier} from '@mattermost/types/products';
|
|||||||
|
|
||||||
import {isCurrentUserGuestUser} from 'mattermost-redux/selectors/entities/users';
|
import {isCurrentUserGuestUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import StatusDropdown from 'components/status_dropdown';
|
|
||||||
import {OnboardingTourSteps, OnboardingTourStepsForGuestUsers} from 'components/tours';
|
import {OnboardingTourSteps, OnboardingTourStepsForGuestUsers} from 'components/tours';
|
||||||
import {
|
import {
|
||||||
CustomizeYourExperienceTour,
|
CustomizeYourExperienceTour,
|
||||||
useShowOnboardingTutorialStep,
|
useShowOnboardingTutorialStep,
|
||||||
} from 'components/tours/onboarding_tour';
|
} from 'components/tours/onboarding_tour';
|
||||||
|
import UserAccountMenu from 'components/user_account_menu';
|
||||||
|
|
||||||
import Pluggable from 'plugins/pluggable';
|
import Pluggable from 'plugins/pluggable';
|
||||||
import {isChannels} from 'utils/products';
|
import {isChannels} from 'utils/products';
|
||||||
@@ -46,10 +46,6 @@ const StyledCustomizeYourExperienceTour = styled.div`
|
|||||||
height: 100%
|
height: 100%
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledStatusDropdown = styled.div`
|
|
||||||
margin-left: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
productId?: ProductIdentifier;
|
productId?: ProductIdentifier;
|
||||||
}
|
}
|
||||||
@@ -87,9 +83,7 @@ const RightControls = ({productId = null}: Props): JSX.Element => {
|
|||||||
</>
|
</>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<StyledStatusDropdown>
|
<UserAccountMenu/>
|
||||||
<StatusDropdown/>
|
|
||||||
</StyledStatusDropdown>
|
|
||||||
</StyledCustomizeYourExperienceTour>
|
</StyledCustomizeYourExperienceTour>
|
||||||
</RightControlsContainer>
|
</RightControlsContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import './menu.scss';
|
||||||
|
|
||||||
export {Menu as Container} from './menu';
|
export {Menu as Container} from './menu';
|
||||||
export {SubMenu} from './sub_menu';
|
export {SubMenu} from './sub_menu';
|
||||||
export {MenuItem as Item} from './menu_item';
|
export {MenuItem as Item} from './menu_item';
|
||||||
|
export type {FirstMenuItemProps} from './menu_item';
|
||||||
export {MenuItemSeparator as Separator} from './menu_item_separator';
|
export {MenuItemSeparator as Separator} from './menu_item_separator';
|
||||||
|
|||||||
28
webapp/channels/src/components/menu/menu.scss
Обычный файл
28
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import MuiMenu from '@mui/material/Menu';
|
||||||
import MuiMenuList from '@mui/material/MenuList';
|
import MuiMenuList from '@mui/material/MenuList';
|
||||||
|
import type {PopoverOrigin} from '@mui/material/Popover';
|
||||||
|
import classNames from 'classnames';
|
||||||
import React, {
|
import React, {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -28,7 +31,8 @@ import Constants, {A11yClassNames} from 'utils/constants';
|
|||||||
import {isKeyPressed} from 'utils/keyboard';
|
import {isKeyPressed} from 'utils/keyboard';
|
||||||
|
|
||||||
import {MenuContext, useMenuContextValue} from './menu_context';
|
import {MenuContext, useMenuContextValue} from './menu_context';
|
||||||
import {MuiMenuStyled} from './menu_styled';
|
|
||||||
|
import './menu.scss';
|
||||||
|
|
||||||
const MENU_OPEN_ANIMATION_DURATION = 150;
|
const MENU_OPEN_ANIMATION_DURATION = 150;
|
||||||
const MENU_CLOSE_ANIMATION_DURATION = 100;
|
const MENU_CLOSE_ANIMATION_DURATION = 100;
|
||||||
@@ -37,6 +41,7 @@ type MenuButtonProps = {
|
|||||||
id: string;
|
id: string;
|
||||||
dateTestId?: string;
|
dateTestId?: string;
|
||||||
'aria-label'?: string;
|
'aria-label'?: string;
|
||||||
|
'aria-describedby'?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
class?: string;
|
class?: string;
|
||||||
as?: keyof JSX.IntrinsicElements;
|
as?: keyof JSX.IntrinsicElements;
|
||||||
@@ -55,18 +60,15 @@ type MenuProps = {
|
|||||||
'aria-label'?: string;
|
'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;
|
onToggle?: (isOpen: boolean) => void;
|
||||||
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>, forceCloseMenu?: () => void) => void;
|
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>, forceCloseMenu?: () => void) => void;
|
||||||
width?: string;
|
width?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultAnchorOrigin = {vertical: 'bottom', horizontal: 'left'};
|
const defaultAnchorOrigin = {vertical: 'bottom', horizontal: 'left'} as PopoverOrigin;
|
||||||
const defaultTransformOrigin = {vertical: 'top', horizontal: 'left'};
|
const defaultTransformOrigin = {vertical: 'top', horizontal: 'left'} as PopoverOrigin;
|
||||||
|
|
||||||
type VerticalOrigin = 'top' | 'center' | 'bottom';
|
|
||||||
type HorizontalOrigin = 'left' | 'center' | 'right';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
menuButton: MenuButtonProps;
|
menuButton: MenuButtonProps;
|
||||||
@@ -76,14 +78,8 @@ interface Props {
|
|||||||
|
|
||||||
// Use MUI Anchor Playgroup to try various anchorOrigin
|
// Use MUI Anchor Playgroup to try various anchorOrigin
|
||||||
// and transformOrigin values - https://mui.com/material-ui/react-popover/#anchor-playground
|
// and transformOrigin values - https://mui.com/material-ui/react-popover/#anchor-playground
|
||||||
anchorOrigin?: {
|
anchorOrigin?: PopoverOrigin;
|
||||||
vertical: VerticalOrigin;
|
transformOrigin?: PopoverOrigin;
|
||||||
horizontal: HorizontalOrigin;
|
|
||||||
};
|
|
||||||
transformOrigin?: {
|
|
||||||
vertical: VerticalOrigin;
|
|
||||||
horizontal: HorizontalOrigin;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,7 +103,7 @@ export function Menu(props: Props) {
|
|||||||
const [disableAutoFocusItem, setDisableAutoFocusItem] = useState(false);
|
const [disableAutoFocusItem, setDisableAutoFocusItem] = useState(false);
|
||||||
const isMenuOpen = Boolean(anchorElement);
|
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<HTMLDivElement>) {
|
function handleMenuClose(event: MouseEvent<HTMLDivElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setAnchorElement(null);
|
setAnchorElement(null);
|
||||||
@@ -125,9 +121,9 @@ export function Menu(props: Props) {
|
|||||||
setAnchorElement(null);
|
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
|
// @see https://github.com/mui/material-ui/issues/32064
|
||||||
function handleMenuClick(e: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
function handleMenuClick(e: MouseEvent<HTMLDivElement> | KeyboardEvent<HTMLDivElement>) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +189,8 @@ export function Menu(props: Props) {
|
|||||||
aria-haspopup={true}
|
aria-haspopup={true}
|
||||||
aria-expanded={isMenuOpen}
|
aria-expanded={isMenuOpen}
|
||||||
disabled={props.menuButton?.disabled ?? false}
|
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 ?? ''}
|
className={props.menuButton?.class ?? ''}
|
||||||
onClick={handleMenuButtonClick}
|
onClick={handleMenuButtonClick}
|
||||||
onMouseDown={handleMenuButtonMouseDown}
|
onMouseDown={handleMenuButtonMouseDown}
|
||||||
@@ -234,21 +231,23 @@ export function Menu(props: Props) {
|
|||||||
<CompassDesignProvider theme={theme}>
|
<CompassDesignProvider theme={theme}>
|
||||||
{renderMenuButton()}
|
{renderMenuButton()}
|
||||||
<MenuContext.Provider value={providerValue}>
|
<MenuContext.Provider value={providerValue}>
|
||||||
<MuiMenuStyled
|
<MuiMenu
|
||||||
anchorEl={anchorElement}
|
anchorEl={anchorElement}
|
||||||
open={isMenuOpen}
|
open={isMenuOpen}
|
||||||
onClose={handleMenuClose}
|
onClose={handleMenuClose}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
onTransitionExited={providerValue.handleClosed}
|
|
||||||
onKeyDown={handleMenuKeyDown}
|
onKeyDown={handleMenuKeyDown}
|
||||||
className={A11yClassNames.POPUP}
|
className={classNames(A11yClassNames.POPUP, 'menu_menuStyled')}
|
||||||
width={props.menu.width}
|
marginThreshold={0}
|
||||||
anchorOrigin={props.anchorOrigin || defaultAnchorOrigin}
|
anchorOrigin={props.anchorOrigin || defaultAnchorOrigin}
|
||||||
transformOrigin={props.transformOrigin || defaultTransformOrigin}
|
transformOrigin={props.transformOrigin || defaultTransformOrigin}
|
||||||
disableAutoFocusItem={disableAutoFocusItem} // This is not anti-pattern, see handleMenuButtonMouseDown
|
disableAutoFocusItem={disableAutoFocusItem} // This is not anti-pattern, see handleMenuButtonMouseDown
|
||||||
MenuListProps={{
|
MenuListProps={{
|
||||||
id: props.menu.id,
|
id: props.menu.id,
|
||||||
'aria-label': props.menu?.['aria-label'] ?? '',
|
'aria-label': props.menu?.['aria-label'] ?? '',
|
||||||
|
style: {
|
||||||
|
width: props.menu.width || 'inherit',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
TransitionProps={{
|
TransitionProps={{
|
||||||
mountOnEnter: true,
|
mountOnEnter: true,
|
||||||
@@ -258,9 +257,12 @@ export function Menu(props: Props) {
|
|||||||
exit: MENU_CLOSE_ANIMATION_DURATION,
|
exit: MENU_CLOSE_ANIMATION_DURATION,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error This exists in source code of mui, but its types are missing
|
||||||
|
onTransitionExited={providerValue.handleClosed}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</MuiMenuStyled>
|
</MuiMenu>
|
||||||
</MenuContext.Provider>
|
</MenuContext.Provider>
|
||||||
</CompassDesignProvider>
|
</CompassDesignProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -89,6 +89,25 @@ export interface Props extends MuiMenuItemProps {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The props for the first menu item to be passed in.
|
||||||
|
* @example
|
||||||
|
* <Menu.Container>
|
||||||
|
* <WrapperOfMenuFirstItem/> <-- Container passes the props to the first item
|
||||||
|
* <Menu.Item/>
|
||||||
|
* </Menu.Container>
|
||||||
|
*/
|
||||||
|
export type FirstMenuItemProps = Omit<
|
||||||
|
Props,
|
||||||
|
| 'onClick'
|
||||||
|
| 'leadingElement'
|
||||||
|
| 'labels'
|
||||||
|
| 'trailingElements'
|
||||||
|
| 'isDestructive'
|
||||||
|
| 'isLabelsRowLayout'
|
||||||
|
| 'children'
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To be used as a child of Menu component.
|
* To be used as a child of Menu component.
|
||||||
* Checkout Compass's Menu Item(compass.mattermost.com) for terminology, styling and usage guidelines.
|
* 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.
|
// 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 (
|
return (
|
||||||
<MenuItemStyled
|
<MenuItemStyled
|
||||||
@@ -283,7 +306,6 @@ export const MenuItemStyled = styled(MuiMenuItem, {
|
|||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
color: isRegular ? 'rgba(var(--center-channel-color-rgb), 0.75)' : 'var(--error-text)',
|
color: isRegular ? 'rgba(var(--center-channel-color-rgb), 0.75)' : 'var(--error-text)',
|
||||||
gap: '4px',
|
gap: '4px',
|
||||||
marginInlineStart: '24px',
|
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
lineHeight: '16px',
|
lineHeight: '16px',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import MuiMenu from '@mui/material/Menu';
|
|
||||||
import type {MenuProps as MuiMenuProps} from '@mui/material/Menu';
|
|
||||||
import {styled} from '@mui/material/styles';
|
|
||||||
|
|
||||||
interface Props extends MuiMenuProps {
|
|
||||||
asSubMenu?: boolean;
|
|
||||||
width?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A styled version of the Material-UI Menu component with few overrides.
|
|
||||||
* @warning This component is meant to be only used inside of the Menu component directory.
|
|
||||||
*/
|
|
||||||
export const MuiMenuStyled = styled(MuiMenu, {
|
|
||||||
shouldForwardProp: (prop) => prop !== 'asSubMenu',
|
|
||||||
})<Props>(
|
|
||||||
({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,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
.menuModal {
|
|
||||||
& .modal-header {
|
|
||||||
& > .close {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .GenericModal__body {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import MuiMenu from '@mui/material/Menu';
|
||||||
import MuiMenuList from '@mui/material/MenuList';
|
import MuiMenuList from '@mui/material/MenuList';
|
||||||
import type {PopoverOrigin} from '@mui/material/Popover';
|
import type {PopoverOrigin} from '@mui/material/Popover';
|
||||||
import React, {
|
import React, {
|
||||||
@@ -31,9 +32,6 @@ import {isKeyPressed} from 'utils/keyboard';
|
|||||||
import {SubMenuContext, useMenuContextValue} from './menu_context';
|
import {SubMenuContext, useMenuContextValue} from './menu_context';
|
||||||
import {MenuItem} from './menu_item';
|
import {MenuItem} from './menu_item';
|
||||||
import type {Props as MenuItemProps} from './menu_item';
|
import type {Props as MenuItemProps} from './menu_item';
|
||||||
import {MuiMenuStyled} from './menu_styled';
|
|
||||||
|
|
||||||
import './sub_menu.scss';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: MenuItemProps['id'];
|
id: MenuItemProps['id'];
|
||||||
@@ -41,10 +39,12 @@ interface Props {
|
|||||||
labels: MenuItemProps['labels'];
|
labels: MenuItemProps['labels'];
|
||||||
trailingElements?: MenuItemProps['trailingElements'];
|
trailingElements?: MenuItemProps['trailingElements'];
|
||||||
isDestructive?: MenuItemProps['isDestructive'];
|
isDestructive?: MenuItemProps['isDestructive'];
|
||||||
|
role?: MenuItemProps['role'];
|
||||||
|
|
||||||
// Menu props
|
// Menu props
|
||||||
menuId: string;
|
menuId: string;
|
||||||
menuAriaLabel?: string;
|
menuAriaLabel?: string;
|
||||||
|
menuAriaDescribedBy?: string;
|
||||||
forceOpenOnLeft?: boolean; // Most of the times this is not needed, since submenu position is calculated and placed
|
forceOpenOnLeft?: boolean; // Most of the times this is not needed, since submenu position is calculated and placed
|
||||||
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -57,8 +57,10 @@ export function SubMenu(props: Props) {
|
|||||||
labels,
|
labels,
|
||||||
trailingElements,
|
trailingElements,
|
||||||
isDestructive,
|
isDestructive,
|
||||||
|
role,
|
||||||
menuId,
|
menuId,
|
||||||
menuAriaLabel,
|
menuAriaLabel,
|
||||||
|
menuAriaDescribedBy,
|
||||||
forceOpenOnLeft,
|
forceOpenOnLeft,
|
||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
@@ -148,6 +150,7 @@ export function SubMenu(props: Props) {
|
|||||||
labels,
|
labels,
|
||||||
trailingElements,
|
trailingElements,
|
||||||
isDestructive,
|
isDestructive,
|
||||||
|
role,
|
||||||
onClick: isMobileView ? handleOnClick : undefined, // OnClicks on parent menuItem of subMenu is only needed in mobile view
|
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}
|
onMouseLeave={handleMouseLeave}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<MuiMenuStyled
|
<MuiMenu
|
||||||
anchorEl={anchorElement}
|
anchorEl={anchorElement}
|
||||||
open={isSubMenuOpen}
|
open={isSubMenuOpen}
|
||||||
asSubMenu={true}
|
|
||||||
anchorOrigin={originOfAnchorAndTransform.anchorOrigin}
|
anchorOrigin={originOfAnchorAndTransform.anchorOrigin}
|
||||||
transformOrigin={originOfAnchorAndTransform.transformOrigin}
|
transformOrigin={originOfAnchorAndTransform.transformOrigin}
|
||||||
sx={{pointerEvents: 'none'}}
|
sx={{pointerEvents: 'none'}}
|
||||||
|
className='menu_menuStyled AsSubMenu'
|
||||||
>
|
>
|
||||||
{/* This component is needed here to re enable pointer events for the submenu items which we had to disable above as */}
|
{/* 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 */}
|
{/* pointer turns to default as soon as it leaves the parent menu */}
|
||||||
@@ -178,6 +181,7 @@ export function SubMenu(props: Props) {
|
|||||||
id={menuId}
|
id={menuId}
|
||||||
component='ul'
|
component='ul'
|
||||||
aria-label={menuAriaLabel}
|
aria-label={menuAriaLabel}
|
||||||
|
aria-describedby={menuAriaDescribedBy}
|
||||||
className={A11yClassNames.POPUP}
|
className={A11yClassNames.POPUP}
|
||||||
onKeyDown={handleSubMenuKeyDown}
|
onKeyDown={handleSubMenuKeyDown}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -190,7 +194,7 @@ export function SubMenu(props: Props) {
|
|||||||
{children}
|
{children}
|
||||||
</SubMenuContext.Provider>
|
</SubMenuContext.Provider>
|
||||||
</MuiMenuList>
|
</MuiMenuList>
|
||||||
</MuiMenuStyled>
|
</MuiMenu>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const CompleteYourProfileTour = () => {
|
|||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|
||||||
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) => {
|
const onDismiss = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -64,6 +64,7 @@ export const CompleteYourProfileTour = () => {
|
|||||||
singleTip={true}
|
singleTip={true}
|
||||||
showOptOut={false}
|
showOptOut={false}
|
||||||
interactivePunchOut={true}
|
interactivePunchOut={true}
|
||||||
|
zIndex={1058}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
switchToChannels,
|
switchToChannels,
|
||||||
} from 'actions/views/onboarding_tasks';
|
} from 'actions/views/onboarding_tasks';
|
||||||
import {setProductMenuSwitcherOpen} from 'actions/views/product_menu';
|
import {setProductMenuSwitcherOpen} from 'actions/views/product_menu';
|
||||||
import {setStatusDropdown} from 'actions/views/status_dropdown';
|
|
||||||
import {getOnboardingTaskPreferences} from 'selectors/onboarding';
|
import {getOnboardingTaskPreferences} from 'selectors/onboarding';
|
||||||
|
|
||||||
import Channels from 'components/common/svg_images_components/channels_svg';
|
import Channels from 'components/common/svg_images_components/channels_svg';
|
||||||
@@ -40,6 +39,7 @@ import {
|
|||||||
} from 'components/tours';
|
} from 'components/tours';
|
||||||
|
|
||||||
import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
|
import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||||
|
import {clickOnMenuButton, MenuButtonIds} from 'utils/keyboard';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ export const useHandleOnBoardingTaskTrigger = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case OnboardingTasksName.COMPLETE_YOUR_PROFILE: {
|
case OnboardingTasksName.COMPLETE_YOUR_PROFILE: {
|
||||||
dispatch(setStatusDropdown(true));
|
clickOnMenuButton(MenuButtonIds.userAccountMenu);
|
||||||
dispatch(setShowOnboardingCompleteProfileTour(true));
|
dispatch(setShowOnboardingCompleteProfileTour(true));
|
||||||
handleSaveData(taskName, TaskNameMapToSteps[taskName].STARTED, true);
|
handleSaveData(taskName, TaskNameMapToSteps[taskName].STARTED, true);
|
||||||
if (inAdminConsole) {
|
if (inAdminConsole) {
|
||||||
|
|||||||
@@ -3,14 +3,15 @@
|
|||||||
|
|
||||||
import React, {useMemo} from 'react';
|
import React, {useMemo} from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
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 {makeGetCustomStatus, showPostHeaderUpdateStatusButton, isCustomStatusEnabled} from 'selectors/views/custom_status';
|
||||||
|
|
||||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||||
import EmojiIcon from 'components/widgets/icons/emoji_icon';
|
import EmojiIcon from 'components/widgets/icons/emoji_icon';
|
||||||
|
|
||||||
|
import {clickOnMenuButton, MenuButtonIds} from 'utils/keyboard';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
@@ -22,7 +23,6 @@ interface ComponentProps {
|
|||||||
const PostHeaderCustomStatus = (props: ComponentProps) => {
|
const PostHeaderCustomStatus = (props: ComponentProps) => {
|
||||||
const getCustomStatus = useMemo(makeGetCustomStatus, []);
|
const getCustomStatus = useMemo(makeGetCustomStatus, []);
|
||||||
const {userId, isSystemMessage, isBot} = props;
|
const {userId, isSystemMessage, isBot} = props;
|
||||||
const dispatch = useDispatch();
|
|
||||||
const userCustomStatus = useSelector((state: GlobalState) => getCustomStatus(state, userId));
|
const userCustomStatus = useSelector((state: GlobalState) => getCustomStatus(state, userId));
|
||||||
const showUpdateStatusButton = useSelector(showPostHeaderUpdateStatusButton);
|
const showUpdateStatusButton = useSelector(showPostHeaderUpdateStatusButton);
|
||||||
const customStatusEnabled = useSelector(isCustomStatusEnabled);
|
const customStatusEnabled = useSelector(isCustomStatusEnabled);
|
||||||
@@ -51,7 +51,7 @@ const PostHeaderCustomStatus = (props: ComponentProps) => {
|
|||||||
|
|
||||||
const updateStatus = (event: React.MouseEvent) => {
|
const updateStatus = (event: React.MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
dispatch(setStatusDropdown(true));
|
clickOnMenuButton(MenuButtonIds.userAccountMenu);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -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);
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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(
|
|
||||||
<StatusDropdown {...baseProps}/>,
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<StatusDropdown {...props}/>,
|
|
||||||
);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should match snapshot with status dropdown open', () => {
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
isStatusDropdownOpen: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallowWithIntl(
|
|
||||||
<StatusDropdown {...props}/>,
|
|
||||||
);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should match snapshot with custom status enabled', () => {
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
isStatusDropdownOpen: true,
|
|
||||||
isCustomStatusEnabled: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallowWithIntl(
|
|
||||||
<StatusDropdown {...props}/>,
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<StatusDropdown {...props}/>,
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<StatusDropdown {...props}/>,
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<StatusDropdown {...props}/>,
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
<StatusDropdown {...props}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
|
||||||
<StatusDropdown {...props}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(wrapper.find('.status-dropdown-menu__clear-container').exists()).toBe(false);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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: <P>(modalData: ModalData<P>) => 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<string, Record<string, MessageDescriptor>> = {
|
|
||||||
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<Props, State> {
|
|
||||||
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 (
|
|
||||||
<Avatar
|
|
||||||
size={size}
|
|
||||||
url={this.props.profilePicture}
|
|
||||||
tabIndex={undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClearStatus = (e: React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLDivElement> | 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 ? (
|
|
||||||
<span className='d-flex'>
|
|
||||||
<CustomStatusEmoji
|
|
||||||
showTooltip={false}
|
|
||||||
emojiStyle={{marginLeft: 0}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<EmojiIcon className={'custom-status-emoji'}/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const pulsatingDot = !isStatusSet && this.props.showCustomStatusPulsatingDot && (
|
|
||||||
<PulsatingDot/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearableTooltipText = (
|
|
||||||
<FormattedMessage
|
|
||||||
id={'input.clear'}
|
|
||||||
defaultMessage='Clear'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearButton = isStatusSet && !pulsatingDot && (
|
|
||||||
<div
|
|
||||||
className={classNames('status-dropdown-menu__clear-container', 'input-clear visible')}
|
|
||||||
onClick={this.handleClearStatus}
|
|
||||||
onTouchEnd={this.handleClearStatus}
|
|
||||||
>
|
|
||||||
<WithTooltip
|
|
||||||
title={clearableTooltipText}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className='input-clear-x'
|
|
||||||
aria-hidden='true'
|
|
||||||
>
|
|
||||||
<i className='icon icon-close-circle'/>
|
|
||||||
</span>
|
|
||||||
</WithTooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const expiryTime = isStatusSet && customStatus?.expires_at && customStatus.duration !== CustomStatusDuration.DONT_CLEAR &&
|
|
||||||
(
|
|
||||||
<ExpiryTime
|
|
||||||
time={customStatus.expires_at}
|
|
||||||
timezone={this.props.timezone}
|
|
||||||
className={classNames('custom_status__expiry', {
|
|
||||||
padded: customStatus?.text?.length > 0,
|
|
||||||
})}
|
|
||||||
withinBrackets={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu.Group>
|
|
||||||
<Menu.ItemToggleModalRedux
|
|
||||||
ariaLabel={customStatusText || customStatusHelpText}
|
|
||||||
modalId={ModalIdentifiers.CUSTOM_STATUS}
|
|
||||||
dialogType={CustomStatusModal}
|
|
||||||
className={classNames('MenuItem__primary-text custom_status__row', {
|
|
||||||
flex: customStatus?.text?.length === 0,
|
|
||||||
})}
|
|
||||||
id={'status-menu-custom-status'}
|
|
||||||
>
|
|
||||||
<span className='custom_status__container'>
|
|
||||||
<span className='custom_status__icon'>
|
|
||||||
{customStatusEmoji}
|
|
||||||
</span>
|
|
||||||
<CustomStatusText
|
|
||||||
text={customStatusText}
|
|
||||||
className='custom_status__text'
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
margin='none'
|
|
||||||
>
|
|
||||||
{customStatusHelpText}
|
|
||||||
</Text>
|
|
||||||
{clearButton}
|
|
||||||
{pulsatingDot}
|
|
||||||
</span>
|
|
||||||
{expiryTime}
|
|
||||||
</Menu.ItemToggleModalRedux>
|
|
||||||
</Menu.Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='custom_status.expiry.until'
|
|
||||||
defaultMessage='Until {time}'
|
|
||||||
values={{time: endTime.format('h:mm A')}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
formattedEndTime = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='custom_status.expiry.until_tomorrow'
|
|
||||||
defaultMessage='Until Tomorrow {time}'
|
|
||||||
values={{time: endTime.format('h:mm A')}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
formattedEndTime = (
|
|
||||||
<FormattedMessage
|
|
||||||
id='custom_status.expiry.until'
|
|
||||||
defaultMessage='Until {time}'
|
|
||||||
values={{time: endTime.format('lll')}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = (
|
|
||||||
<CheckIcon
|
|
||||||
size={16}
|
|
||||||
color={'var(--button-bg)'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
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<MenuType>(({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}
|
|
||||||
<span className={`dndTime-${id}_timestamp`}>
|
|
||||||
<FormattedDate
|
|
||||||
value={tomorrow}
|
|
||||||
weekday='short'
|
|
||||||
timeZone={this.props.timezone}
|
|
||||||
/>
|
|
||||||
{', '}
|
|
||||||
<FormattedTime
|
|
||||||
value={tomorrow}
|
|
||||||
timeStyle='short'
|
|
||||||
hour12={!this.props.isMilitaryTime}
|
|
||||||
timeZone={this.props.timezone}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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 (
|
|
||||||
<MenuWrapper
|
|
||||||
onToggle={this.onToggle}
|
|
||||||
open={this.props.isStatusDropdownOpen}
|
|
||||||
className={classNames('status-dropdown-menu status-dropdown-menu-global-header', {
|
|
||||||
active: this.props.isStatusDropdownOpen || isStatusSet,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className='status-wrapper style--none'
|
|
||||||
aria-label={menuAriaLabeltext}
|
|
||||||
aria-expanded={this.props.isStatusDropdownOpen}
|
|
||||||
aria-controls='statusDropdownMenu'
|
|
||||||
>
|
|
||||||
<CustomStatusEmoji
|
|
||||||
showTooltip={true}
|
|
||||||
emojiStyle={{marginRight: '6px'}}
|
|
||||||
onClick={this.handleCustomStatusEmojiClick as () => void}
|
|
||||||
/>
|
|
||||||
{this.renderProfilePicture('sm')}
|
|
||||||
<div
|
|
||||||
className='status'
|
|
||||||
>
|
|
||||||
<StatusIcon
|
|
||||||
size={'sm'}
|
|
||||||
status={(this.props.status || 'offline') as TUserStatus}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<Menu
|
|
||||||
ariaLabel={this.props.intl.formatMessage({id: 'status_dropdown.menuAriaLabel', defaultMessage: 'Set a status'})}
|
|
||||||
id={'statusDropdownMenu'}
|
|
||||||
listId={'status-drop-down-menu-list'}
|
|
||||||
>
|
|
||||||
{currentUser && (
|
|
||||||
<Menu.Header onClick={this.openProfileModal}>
|
|
||||||
{this.renderProfilePicture('lg')}
|
|
||||||
<div className={'username-wrapper'}>
|
|
||||||
<Text
|
|
||||||
className={'bold'}
|
|
||||||
margin={'none'}
|
|
||||||
>{`${currentUser.first_name} ${currentUser.last_name}`}</Text>
|
|
||||||
<Text
|
|
||||||
margin={'none'}
|
|
||||||
className={!currentUser.first_name && !currentUser.last_name ? 'bold' : 'contrast'}
|
|
||||||
color={!currentUser.first_name && !currentUser.last_name ? undefined : 'inherit'}
|
|
||||||
>
|
|
||||||
{'@' + currentUser.username}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Menu.Header>
|
|
||||||
)}
|
|
||||||
<Menu.Group>
|
|
||||||
<Menu.ItemAction
|
|
||||||
show={this.isUserOutOfOffice()}
|
|
||||||
onClick={setOnline}
|
|
||||||
ariaLabel={this.props.intl.formatMessage(statusDropdownMessages.ooo.name)}
|
|
||||||
text={this.props.intl.formatMessage(statusDropdownMessages.ooo.name)}
|
|
||||||
icon={(
|
|
||||||
<CancelIcon
|
|
||||||
color={'rgba(var(--center-channel-color-rgb), 0.56)'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
extraText={this.props.intl.formatMessage(statusDropdownMessages.ooo.extra)}
|
|
||||||
rightDecorator={selectedIndicator}
|
|
||||||
/>
|
|
||||||
</Menu.Group>
|
|
||||||
{customStatusComponent}
|
|
||||||
<Menu.Group>
|
|
||||||
<Menu.ItemAction
|
|
||||||
onClick={setOnline}
|
|
||||||
ariaLabel={this.props.intl.formatMessage(statusDropdownMessages.online.name)}
|
|
||||||
text={this.props.intl.formatMessage(statusDropdownMessages.online.name)}
|
|
||||||
icon={(
|
|
||||||
<StatusIcon
|
|
||||||
status={'online'}
|
|
||||||
className={'status-icon'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
rightDecorator={status === 'online' && selectedIndicator}
|
|
||||||
id={'status-menu-online'}
|
|
||||||
/>
|
|
||||||
<Menu.ItemAction
|
|
||||||
onClick={setAway}
|
|
||||||
ariaLabel={this.props.intl.formatMessage(statusDropdownMessages.away.name)}
|
|
||||||
text={this.props.intl.formatMessage(statusDropdownMessages.away.name)}
|
|
||||||
icon={(
|
|
||||||
<StatusIcon
|
|
||||||
status={'away'}
|
|
||||||
className={'status-icon'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
rightDecorator={status === 'away' && selectedIndicator}
|
|
||||||
id={'status-menu-away'}
|
|
||||||
/>
|
|
||||||
<Menu.ItemSubMenu
|
|
||||||
subMenu={dndSubMenuItems}
|
|
||||||
ariaLabel={`${this.props.intl.formatMessage(statusDropdownMessages.dnd.name)}. ${dndExtraText}`}
|
|
||||||
text={this.props.intl.formatMessage(statusDropdownMessages.dnd.name)}
|
|
||||||
extraText={dndExtraText}
|
|
||||||
icon={(
|
|
||||||
<StatusIcon
|
|
||||||
status={'dnd'}
|
|
||||||
className={'status-icon'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
rightDecorator={status === 'dnd' && selectedIndicator}
|
|
||||||
direction={'left'}
|
|
||||||
openUp={this.state.openUp}
|
|
||||||
id={'status-menu-dnd'}
|
|
||||||
action={() => setDnd(0)}
|
|
||||||
/>
|
|
||||||
<Menu.ItemAction
|
|
||||||
onClick={setOffline}
|
|
||||||
ariaLabel={this.props.intl.formatMessage(statusDropdownMessages.offline.name)}
|
|
||||||
text={this.props.intl.formatMessage(statusDropdownMessages.offline.name)}
|
|
||||||
icon={(
|
|
||||||
<StatusIcon
|
|
||||||
status={'offline'}
|
|
||||||
className={'status-icon'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
rightDecorator={status === 'offline' && selectedIndicator}
|
|
||||||
id={'status-menu-offline'}
|
|
||||||
/>
|
|
||||||
</Menu.Group>
|
|
||||||
<Menu.Group>
|
|
||||||
<Menu.ItemToggleModalRedux
|
|
||||||
id='accountSettings'
|
|
||||||
ariaLabel='Profile'
|
|
||||||
modalId={ModalIdentifiers.USER_SETTINGS}
|
|
||||||
dialogType={UserSettingsModal}
|
|
||||||
dialogProps={{isContentProductSettings: false}}
|
|
||||||
text={this.props.intl.formatMessage({id: 'navbar_dropdown.profileSettings', defaultMessage: 'Profile'})}
|
|
||||||
icon={
|
|
||||||
<AccountOutlineIcon
|
|
||||||
size={16}
|
|
||||||
color={'rgba(var(--center-channel-color-rgb), 0.56)'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{this.props.showCompleteYourProfileTour && (
|
|
||||||
<div
|
|
||||||
onClick={this.handleCompleteYourProfileTask}
|
|
||||||
className={'account-settings-complete'}
|
|
||||||
>
|
|
||||||
<CompleteYourProfileTour/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Menu.ItemToggleModalRedux>
|
|
||||||
</Menu.Group>
|
|
||||||
<Menu.Group>
|
|
||||||
<span className={'logout__icon'}>
|
|
||||||
<Menu.ItemAction
|
|
||||||
id='logout'
|
|
||||||
onClick={this.handleEmitUserLoggedOutEvent}
|
|
||||||
text={this.props.intl.formatMessage({id: 'navbar_dropdown.logout', defaultMessage: 'Log Out'})}
|
|
||||||
icon={
|
|
||||||
<ExitToAppIcon
|
|
||||||
size={16}
|
|
||||||
color={'rgba(var(--center-channel-color-rgb), 0.56)'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Menu.Group>
|
|
||||||
</Menu>
|
|
||||||
</MenuWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export default injectIntl(StatusDropdown);
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`UserAccountNameMenuItem should not break if no props are passed 1`] = `
|
||||||
|
<div>
|
||||||
|
<li
|
||||||
|
aria-haspopup="true"
|
||||||
|
class="MuiButtonBase-root-JvZdr dKFJFs MuiButtonBase-root MuiMenuItem-root MuiMenuItem-gutters MuiMenuItem-root-dXqYNm kIRdVO MuiMenuItem-root MuiMenuItem-gutters sc-gswNZR cUFZeQ userAccountMenu_nameMenuItem"
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="leading-element"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="user profile image"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="Avatar Avatar-lg"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="label-elements"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="userAccountMenu_nameMenuItem_primaryLabel"
|
||||||
|
>
|
||||||
|
@undefined
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
61
webapp/channels/src/components/user_account_menu/index.ts
Обычный файл
61
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<typeof connector>;
|
||||||
|
|
||||||
|
export default connector(UserAccountMenu);
|
||||||
@@ -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 (
|
||||||
|
<CheckIcon
|
||||||
|
size={16}
|
||||||
|
className='userAccountMenu_menuItemTrailingCheckIcon'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [props.isStatusAway]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
leadingElement={
|
||||||
|
<ClockIcon
|
||||||
|
size='18'
|
||||||
|
className='userAccountMenu_awayMenuItem_icon'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.awayMenuItem.label'
|
||||||
|
defaultMessage='Away'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
trailingElements={trailingElement}
|
||||||
|
aria-checked={props.isStatusAway}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import type {MouseEvent, TouchEvent, KeyboardEvent, ReactNode} from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {PulsatingDot} from '@mattermost/components';
|
||||||
|
import {type UserCustomStatus, CustomStatusDuration} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {unsetCustomStatus} from 'mattermost-redux/actions/users';
|
||||||
|
|
||||||
|
import {showStatusDropdownPulsatingDot} from 'selectors/views/custom_status';
|
||||||
|
|
||||||
|
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||||
|
import CustomStatusText from 'components/custom_status/custom_status_text';
|
||||||
|
import ExpiryTime from 'components/custom_status/expiry_time';
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
import WithTooltip from 'components/with_tooltip';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
timezone?: string;
|
||||||
|
customStatus?: UserCustomStatus;
|
||||||
|
openCustomStatusModal: (event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserAccountSetCustomStatusMenuItem(props: Props) {
|
||||||
|
const {formatMessage, formatTime, formatDate} = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const showPulsatingDot = useSelector(showStatusDropdownPulsatingDot);
|
||||||
|
|
||||||
|
function handleClear(event: MouseEvent<HTMLElement> | TouchEvent) {
|
||||||
|
event.stopPropagation();
|
||||||
|
dispatch(unsetCustomStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasStatusWithText = Boolean(props.customStatus?.text?.length);
|
||||||
|
const hasStatusWithExpiry = props.customStatus?.duration !== CustomStatusDuration.DONT_CLEAR && props.customStatus?.expires_at;
|
||||||
|
const hasStatusWithNoExpiry = props.customStatus?.duration === CustomStatusDuration.DONT_CLEAR;
|
||||||
|
|
||||||
|
let label;
|
||||||
|
let ariaDescription: string;
|
||||||
|
if (hasStatusWithText && hasStatusWithExpiry) {
|
||||||
|
label = (
|
||||||
|
<>
|
||||||
|
<CustomStatusText
|
||||||
|
text={props.customStatus?.text}
|
||||||
|
/>
|
||||||
|
<ExpiryTime
|
||||||
|
time={props.customStatus?.expires_at ?? ''}
|
||||||
|
timezone={props.timezone}
|
||||||
|
withinBrackets={true}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
ariaDescription = formatMessage({
|
||||||
|
id: 'userAccountMenu.setCustomStatusMenuItem.hasStatusWithTextAndExpiry.ariaDescription',
|
||||||
|
defaultMessage: 'Status is "{text}" and expires at {time}. Set a custom status.',
|
||||||
|
}, {
|
||||||
|
text: props.customStatus?.text,
|
||||||
|
time: `${formatTime(props.customStatus?.expires_at)} ${formatDate(props.customStatus?.expires_at)}`,
|
||||||
|
});
|
||||||
|
} else if (hasStatusWithText && hasStatusWithNoExpiry) {
|
||||||
|
label = (
|
||||||
|
<CustomStatusText
|
||||||
|
text={props.customStatus?.text}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
ariaDescription = formatMessage({
|
||||||
|
id: 'userAccountMenu.setCustomStatusMenuItem.hasStatusWithTextAndNoExpiry.ariaDescription',
|
||||||
|
defaultMessage: 'Status is "{text}". Set a custom status.',
|
||||||
|
}, {
|
||||||
|
text: props.customStatus?.text,
|
||||||
|
});
|
||||||
|
} else if (!hasStatusWithText && hasStatusWithExpiry) {
|
||||||
|
label = (
|
||||||
|
<ExpiryTime
|
||||||
|
time={props.customStatus?.expires_at ?? ''}
|
||||||
|
timezone={props.timezone}
|
||||||
|
withinBrackets={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
ariaDescription = formatMessage({
|
||||||
|
id: 'userAccountMenu.setCustomStatusMenuItem.hasStatusWithExpiryAndNoText.ariaDescription',
|
||||||
|
defaultMessage: 'Status expires at {time}. Set a custom status.',
|
||||||
|
}, {
|
||||||
|
time: `${formatTime(props.customStatus?.expires_at)} ${formatDate(props.customStatus?.expires_at)}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.setCustomStatusMenuItem.noStatusTextSet'
|
||||||
|
defaultMessage='Set custom status text'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
ariaDescription = formatMessage({
|
||||||
|
id: 'userAccountMenu.setCustomStatusMenuItem.noStatusTextSet',
|
||||||
|
defaultMessage: 'Set custom status text',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let trailingElement: ReactNode | null = null;
|
||||||
|
if (showPulsatingDot) {
|
||||||
|
trailingElement = <PulsatingDot/>;
|
||||||
|
} else if (hasStatusWithText || hasStatusWithExpiry || hasStatusWithNoExpiry) {
|
||||||
|
trailingElement = (
|
||||||
|
<WithTooltip
|
||||||
|
title={formatMessage({id: 'userAccountMenu.setCustomStatusMenuItem.clearTooltip', defaultMessage: 'Clear custom status'})}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className='icon icon-close-circle userAccountMenu_menuItemTrailingIconClear'
|
||||||
|
aria-hidden='true'
|
||||||
|
onClick={handleClear}
|
||||||
|
/>
|
||||||
|
</WithTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
className={classNames('userAccountMenu_customStatusMenuItem', {
|
||||||
|
hasTrailingElement: Boolean(trailingElement),
|
||||||
|
})}
|
||||||
|
leadingElement={
|
||||||
|
<CustomStatusEmoji
|
||||||
|
showTooltip={false}
|
||||||
|
emojiStyle={{marginLeft: 0}}
|
||||||
|
emojiSize={16}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labels={label}
|
||||||
|
trailingElements={trailingElement}
|
||||||
|
aria-description={ariaDescription}
|
||||||
|
onClick={props.openCustomStatusModal}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, {useMemo} from 'react';
|
||||||
|
import type {MouseEvent, KeyboardEvent} from 'react';
|
||||||
|
import {FormattedDate, FormattedMessage, FormattedTime, useIntl} from 'react-intl';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {CheckIcon, ChevronRightIcon, MinusCircleIcon} from '@mattermost/compass-icons/components';
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {setStatus} from 'mattermost-redux/actions/users';
|
||||||
|
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {getDndEndTimeForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
|
import {openModal} from 'actions/views/modals';
|
||||||
|
|
||||||
|
import DndCustomTimePicker from 'components/dnd_custom_time_picker_modal';
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
import ResetStatusModal from 'components/reset_status_modal';
|
||||||
|
|
||||||
|
import {ModalIdentifiers, Preferences, UserStatuses} from 'utils/constants';
|
||||||
|
import {getCurrentMomentForTimezone, getBrowserTimezone, getCurrentDateTimeForTimezone} from 'utils/timezone';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
userId: UserProfile['id'];
|
||||||
|
timezone?: string;
|
||||||
|
shouldConfirmBeforeStatusChange: boolean;
|
||||||
|
isStatusDnd: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserAccountDndMenuItem(props: Props) {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dndEndTime = useSelector((state: GlobalState) => getDndEndTimeForUserId(state, props.userId));
|
||||||
|
|
||||||
|
const isMilitaryTime = useSelector((state: GlobalState) => getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false));
|
||||||
|
|
||||||
|
const tomorrow9AMDateObject = getCurrentMomentForTimezone(props.timezone).add(1, 'day').set({hour: 9, minute: 0}).toDate();
|
||||||
|
|
||||||
|
function openCustomTimePicker() {
|
||||||
|
if (props.shouldConfirmBeforeStatusChange) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.RESET_STATUS,
|
||||||
|
dialogType: ResetStatusModal,
|
||||||
|
dialogProps: {
|
||||||
|
newStatus: UserStatuses.DND,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.DND_CUSTOM_TIME_PICKER,
|
||||||
|
dialogType: DndCustomTimePicker,
|
||||||
|
dialogProps: {
|
||||||
|
currentDate: props.timezone ? getCurrentDateTimeForTimezone(props.timezone) : new Date(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubMenuItemClick(event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
||||||
|
if (props.shouldConfirmBeforeStatusChange) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.RESET_STATUS,
|
||||||
|
dialogType: ResetStatusModal,
|
||||||
|
dialogProps: {
|
||||||
|
newStatus: UserStatuses.DND,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {currentTarget: {id}} = event;
|
||||||
|
|
||||||
|
const currentDate = getCurrentMomentForTimezone(props.timezone);
|
||||||
|
|
||||||
|
let endTime = currentDate;
|
||||||
|
switch (id) {
|
||||||
|
case DND_SUB_MENU_ITEMS_IDS.DO_NOT_CLEAR:
|
||||||
|
endTime = moment(0);
|
||||||
|
break;
|
||||||
|
case DND_SUB_MENU_ITEMS_IDS.THIRTY_MINUTES:
|
||||||
|
// add 30 minutes in current time
|
||||||
|
endTime = currentDate.add(30, 'minutes');
|
||||||
|
break;
|
||||||
|
case DND_SUB_MENU_ITEMS_IDS.ONE_HOUR:
|
||||||
|
// add 1 hour in current time
|
||||||
|
endTime = currentDate.add(1, 'hour');
|
||||||
|
break;
|
||||||
|
case DND_SUB_MENU_ITEMS_IDS.TWO_HOURS:
|
||||||
|
// add 2 hours in current time
|
||||||
|
endTime = currentDate.add(2, 'hours');
|
||||||
|
break;
|
||||||
|
case DND_SUB_MENU_ITEMS_IDS.TOMORROW:
|
||||||
|
// set to next day 9 in the morning
|
||||||
|
endTime = currentDate.add(1, 'day').set({hour: 9, minute: 0});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setStatus({
|
||||||
|
user_id: props.userId,
|
||||||
|
status: UserStatuses.DND,
|
||||||
|
dnd_end_time: endTime.utc().unix(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const trailingElement = useMemo(() => {
|
||||||
|
if (props.isStatusDnd) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CheckIcon
|
||||||
|
size={16}
|
||||||
|
className='userAccountMenu_menuItemTrailingCheckIcon'
|
||||||
|
/>
|
||||||
|
<ChevronRightIcon size={16}/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChevronRightIcon size={16}/>
|
||||||
|
);
|
||||||
|
}, [props.isStatusDnd]);
|
||||||
|
|
||||||
|
// This function has dual purpose, first it returns the secondary label for the menu item and
|
||||||
|
// second it returns the part of the aria label for the menu item.
|
||||||
|
function getSecondaryLabel(isStatusDnd: boolean, dndEndTime?: number, timezone?: string) {
|
||||||
|
if (!isStatusDnd) {
|
||||||
|
return formatMessage({
|
||||||
|
id: 'userAccountMenu.dndMenuItem.secondaryLabel',
|
||||||
|
defaultMessage: 'Disables all notifications',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// When DND is set to not clear
|
||||||
|
if (!dndEndTime || dndEndTime === 0) {
|
||||||
|
return formatMessage({
|
||||||
|
id: 'userAccountMenu.dndMenuItem.secondaryLabel.doNotClear',
|
||||||
|
defaultMessage: 'Until indefinitely',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tz = timezone || getBrowserTimezone();
|
||||||
|
const currentTime = moment().tz(tz);
|
||||||
|
const endTime = moment.unix(dndEndTime).tz(tz);
|
||||||
|
|
||||||
|
const diffDays = endTime.clone().startOf('day').diff(currentTime.clone().startOf('day'), 'days');
|
||||||
|
|
||||||
|
if (diffDays === 0) {
|
||||||
|
return formatMessage({
|
||||||
|
id: 'userAccountMenu.dndMenuItem.secondaryLabel.untilTodaySomeTime',
|
||||||
|
defaultMessage: 'Until {time}',
|
||||||
|
}, {
|
||||||
|
time: endTime.format('h:mm A'),
|
||||||
|
});
|
||||||
|
} else if (diffDays === 1) {
|
||||||
|
return formatMessage({
|
||||||
|
id: 'userAccountMenu.dndMenuItem.secondaryLabel.untilTomorrowSomeTime',
|
||||||
|
defaultMessage: 'Until tomorrow {time}',
|
||||||
|
}, {
|
||||||
|
time: endTime.format('h:mm A'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatMessage({
|
||||||
|
id: 'userAccountMenu.dndMenuItem.secondaryLabel.untilLaterSomeTime',
|
||||||
|
defaultMessage: 'Until {time}',
|
||||||
|
}, {
|
||||||
|
time: endTime.format('lll'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.SubMenu
|
||||||
|
id='userAccountMenu.dndMenuItem'
|
||||||
|
menuId='userAccountMenu.dndSubMenu'
|
||||||
|
menuAriaDescribedBy='userAccountMenu_dndSubMenuTitle'
|
||||||
|
leadingElement={
|
||||||
|
<MinusCircleIcon
|
||||||
|
size='18'
|
||||||
|
className='userAccountMenu_dndMenuItem_icon'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labels={
|
||||||
|
<>
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.dndMenuItem.primaryLabel'
|
||||||
|
defaultMessage='Do not disturb'
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{getSecondaryLabel(props.isStatusDnd, dndEndTime, props.timezone)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
role='menuitemradio' // Prevents menu item from closing, not a recommended solution
|
||||||
|
aria-checked={props.isStatusDnd}
|
||||||
|
trailingElements={trailingElement}
|
||||||
|
>
|
||||||
|
<h5
|
||||||
|
id='userAccountMenu_dndSubMenuTitle'
|
||||||
|
className='userAccountMenu_dndMenuItem_subMenuTitle'
|
||||||
|
aria-hidden={true}
|
||||||
|
>
|
||||||
|
{formatMessage({
|
||||||
|
id: 'userAccountMenu.dndSubMenu.title',
|
||||||
|
defaultMessage: 'Clear after:',
|
||||||
|
})}
|
||||||
|
</h5>
|
||||||
|
<Menu.Item
|
||||||
|
id={DND_SUB_MENU_ITEMS_IDS.DO_NOT_CLEAR}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.dndSubMenuItem.doNotClear'
|
||||||
|
defaultMessage="Don't clear"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={handleSubMenuItemClick}
|
||||||
|
/>
|
||||||
|
<Menu.Item
|
||||||
|
id={DND_SUB_MENU_ITEMS_IDS.THIRTY_MINUTES}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.dndSubMenuItem.30Minutes'
|
||||||
|
defaultMessage='30 mins'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={handleSubMenuItemClick}
|
||||||
|
/>
|
||||||
|
<Menu.Item
|
||||||
|
id={DND_SUB_MENU_ITEMS_IDS.ONE_HOUR}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.dndSubMenuItem.1Hour'
|
||||||
|
defaultMessage='1 hour'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={handleSubMenuItemClick}
|
||||||
|
/>
|
||||||
|
<Menu.Item
|
||||||
|
id={DND_SUB_MENU_ITEMS_IDS.TWO_HOURS}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.dndSubMenuItem.2Hours'
|
||||||
|
defaultMessage='2 hours'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={handleSubMenuItemClick}
|
||||||
|
/>
|
||||||
|
<Menu.Item
|
||||||
|
id={DND_SUB_MENU_ITEMS_IDS.TOMORROW}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.dndSubMenuItem.tomorrow'
|
||||||
|
defaultMessage='Tomorrow'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
trailingElements={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.dndSubMenuItem.tomorrowsDateTime'
|
||||||
|
defaultMessage='{shortDay}, {shortTime}'
|
||||||
|
values={{
|
||||||
|
shortDay: (
|
||||||
|
<FormattedDate
|
||||||
|
value={tomorrow9AMDateObject}
|
||||||
|
weekday='short'
|
||||||
|
timeZone={props.timezone}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
shortTime: (
|
||||||
|
<FormattedTime
|
||||||
|
value={tomorrow9AMDateObject}
|
||||||
|
timeStyle='short'
|
||||||
|
hour12={!isMilitaryTime}
|
||||||
|
timeZone={props.timezone}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={handleSubMenuItemClick}
|
||||||
|
/>
|
||||||
|
<Menu.Item
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.dndSubMenuItem.custom'
|
||||||
|
defaultMessage='Choose date and time'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={openCustomTimePicker}
|
||||||
|
/>
|
||||||
|
</Menu.SubMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DND_SUB_MENU_ITEMS_IDS = {
|
||||||
|
DO_NOT_CLEAR: 'userAccountMenu.dndSubMenuItem.doNotClear',
|
||||||
|
THIRTY_MINUTES: 'userAccountMenu.dndSubMenuItem.30Minutes',
|
||||||
|
ONE_HOUR: 'userAccountMenu.dndSubMenuItem.1Hour',
|
||||||
|
TWO_HOURS: 'userAccountMenu.dndSubMenuItem.2Hours',
|
||||||
|
TOMORROW: 'userAccountMenu.dndSubMenuItem.tomorrow',
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import {ExitToAppIcon} from '@mattermost/compass-icons/components';
|
||||||
|
|
||||||
|
import {emitUserLoggedOutEvent} from 'actions/global_actions';
|
||||||
|
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
|
||||||
|
export default function UserAccountLogoutMenuItem() {
|
||||||
|
function handleClick() {
|
||||||
|
emitUserLoggedOutEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
leadingElement={
|
||||||
|
<ExitToAppIcon
|
||||||
|
size={18}
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.logoutMenuItem.label'
|
||||||
|
defaultMessage='Log out'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
@import "utils/_mixins";
|
||||||
|
|
||||||
|
#userAccountMenu {
|
||||||
|
.userAccountMenu_nameMenuItem {
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.leading-element {
|
||||||
|
width: unset;
|
||||||
|
height: unset;
|
||||||
|
margin-inline-end: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-elements {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.userAccountMenu_nameMenuItem_primaryLabel {
|
||||||
|
max-width: 150px;
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Metropolis', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 22px;
|
||||||
|
@include textEllipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.userAccountMenu_nameMenuItem_secondaryLabel {
|
||||||
|
max-width: 150px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
@include textEllipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_setCustomStatusMenuItem {
|
||||||
|
.userAccountMenu_setCustomStatusMenuItem_icon > svg {
|
||||||
|
fill: rgba(var(--center-channel-color-rgb), 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This aligns the icon in the center, emoji icon is wrapped in a span
|
||||||
|
// thats why we need to align the span as well
|
||||||
|
> .leading-element, .userAccountMenu_setCustomStatusMenuItem_icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-elements > span {
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_customStatusMenuItem {
|
||||||
|
&.hasTrailingElement {
|
||||||
|
.label-elements {
|
||||||
|
> span {
|
||||||
|
max-width: 156px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-elements {
|
||||||
|
> span {
|
||||||
|
max-width: 196px;
|
||||||
|
@include textEllipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_menuItemTrailingCheckIcon {
|
||||||
|
color: var(--button-bg);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_menuItemTrailingIconClear {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_profileMenuItem_tourWrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_dndMenuItem_subMenuTitle {
|
||||||
|
padding: 6px 20px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--center-channel-color);
|
||||||
|
font-weight: bold;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_menuButton {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
height: auto;
|
||||||
|
align-items: center;
|
||||||
|
align-self: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
|
||||||
|
&:hover, &[aria-expanded="true"] {
|
||||||
|
background: var(--sidebar-text-08);
|
||||||
|
|
||||||
|
.userStatusIconWrapper {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(var(--global-header-text-rgb), 0.08),
|
||||||
|
rgba(var(--global-header-text-rgb), 0.08)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.withCustomStatus {
|
||||||
|
background: rgba(var(--global-header-text-rgb), 0.16);
|
||||||
|
|
||||||
|
.userStatusIconWrapper {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(var(--global-header-text-rgb), 0.16),
|
||||||
|
rgba(var(--global-header-text-rgb), 0.16)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userStatusIconWrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: auto;
|
||||||
|
right: 3px;
|
||||||
|
bottom: 1px;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 100px;
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--global-header-background);
|
||||||
|
color: var(--global-header-text);
|
||||||
|
line-height: 0;
|
||||||
|
visibility: visible;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_onlineMenuItem_icon {
|
||||||
|
color: var(--online-indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_awayMenuItem_icon {
|
||||||
|
color: var(--away-indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_dndMenuItem_icon {
|
||||||
|
color: var(--dnd-indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAccountMenu_offlineMenuItem_icon {
|
||||||
|
color: rgba(141, 147, 165, 0.56);
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import type {MouseEvent, KeyboardEvent} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
|
import CustomStatusModal from 'components/custom_status/custom_status_modal';
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
|
||||||
|
import {ModalIdentifiers, UserStatuses} from 'utils/constants';
|
||||||
|
|
||||||
|
import UserAccountAwayMenuItem from './user_account_away_menuitem';
|
||||||
|
import UserAccountCustomStatusMenuItem from './user_account_custom_status_menuitem';
|
||||||
|
import UserAccountDndMenuItem from './user_account_dnd_menuitem';
|
||||||
|
import UserAccountLogoutMenuItem from './user_account_logout_menuitem';
|
||||||
|
import UserAccountMenuButton from './user_account_menuButton';
|
||||||
|
import UserAccountNameMenuItem from './user_account_name_menuitem';
|
||||||
|
import UserAccountOfflineMenuItem from './user_account_offline_menuitem';
|
||||||
|
import UserAccountOnlineMenuItem from './user_account_online_menuitem';
|
||||||
|
import UserAccountOutOfOfficeMenuItem from './user_account_out_of_office_menuitem';
|
||||||
|
import UserAccountProfileMenuItem from './user_account_profile_menuitem';
|
||||||
|
import UserAccountSetCustomStatusMenuItem from './user_account_set_custom_status_menuitem';
|
||||||
|
|
||||||
|
import type {PropsFromRedux} from './index';
|
||||||
|
|
||||||
|
import './user_account_menu.scss';
|
||||||
|
|
||||||
|
type Props = PropsFromRedux;
|
||||||
|
|
||||||
|
export default function UserAccountMenu(props: Props) {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
function openCustomStatusModal(event: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>) {
|
||||||
|
event.stopPropagation();
|
||||||
|
props.actions.openModal({
|
||||||
|
modalId: ModalIdentifiers.CUSTOM_STATUS,
|
||||||
|
dialogType: CustomStatusModal,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCustomStatusSet = !props.isCustomStatusExpired && props.customStatus && (props.customStatus.text?.length > 0 || props.customStatus.emoji?.length > 0);
|
||||||
|
const shouldConfirmBeforeStatusChange = props.autoResetPref === '' && props.status === UserStatuses.OUT_OF_OFFICE;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Container
|
||||||
|
menuButton={{
|
||||||
|
id: 'userAccountMenuButton',
|
||||||
|
class: classNames('userAccountMenu_menuButton', {
|
||||||
|
withCustomStatus: isCustomStatusSet,
|
||||||
|
}),
|
||||||
|
'aria-label': formatMessage({id: 'userAccountMenu.menuButton.ariaLabel', defaultMessage: 'User\'s account menu'}),
|
||||||
|
'aria-describedby': 'userAccountMenuButtonDescribedBy',
|
||||||
|
children: (
|
||||||
|
<UserAccountMenuButton
|
||||||
|
profilePicture={props.profilePicture}
|
||||||
|
openCustomStatusModal={openCustomStatusModal}
|
||||||
|
status={props.status}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
menu={{
|
||||||
|
id: 'userAccountMenu',
|
||||||
|
width: '264px',
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserAccountNameMenuItem
|
||||||
|
profilePicture={props.profilePicture}
|
||||||
|
/>
|
||||||
|
<Menu.Separator/>
|
||||||
|
{props.status === UserStatuses.OUT_OF_OFFICE && (
|
||||||
|
<UserAccountOutOfOfficeMenuItem
|
||||||
|
userId={props.userId}
|
||||||
|
shouldConfirmBeforeStatusChange={shouldConfirmBeforeStatusChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{props.status === UserStatuses.OUT_OF_OFFICE && (
|
||||||
|
<Menu.Separator/>
|
||||||
|
)}
|
||||||
|
{props.isCustomStatusEnabled && !isCustomStatusSet && (
|
||||||
|
<UserAccountSetCustomStatusMenuItem
|
||||||
|
openCustomStatusModal={openCustomStatusModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{props.isCustomStatusEnabled && isCustomStatusSet && (
|
||||||
|
<UserAccountCustomStatusMenuItem
|
||||||
|
timezone={props.timezone}
|
||||||
|
customStatus={props.customStatus}
|
||||||
|
openCustomStatusModal={openCustomStatusModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{props.isCustomStatusEnabled && (
|
||||||
|
<Menu.Separator/>
|
||||||
|
)}
|
||||||
|
<UserAccountOnlineMenuItem
|
||||||
|
userId={props.userId}
|
||||||
|
shouldConfirmBeforeStatusChange={shouldConfirmBeforeStatusChange}
|
||||||
|
isStatusOnline={props.status === UserStatuses.ONLINE}
|
||||||
|
/>
|
||||||
|
<UserAccountAwayMenuItem
|
||||||
|
userId={props.userId}
|
||||||
|
shouldConfirmBeforeStatusChange={shouldConfirmBeforeStatusChange}
|
||||||
|
isStatusAway={props.status === UserStatuses.AWAY}
|
||||||
|
/>
|
||||||
|
<UserAccountDndMenuItem
|
||||||
|
userId={props.userId}
|
||||||
|
shouldConfirmBeforeStatusChange={shouldConfirmBeforeStatusChange}
|
||||||
|
timezone={props.timezone}
|
||||||
|
isStatusDnd={props.status === UserStatuses.DND}
|
||||||
|
/>
|
||||||
|
<UserAccountOfflineMenuItem
|
||||||
|
userId={props.userId}
|
||||||
|
shouldConfirmBeforeStatusChange={shouldConfirmBeforeStatusChange}
|
||||||
|
isStatusOffline={props.status === UserStatuses.OFFLINE}
|
||||||
|
/>
|
||||||
|
<Menu.Separator/>
|
||||||
|
<UserAccountProfileMenuItem
|
||||||
|
userId={props.userId}
|
||||||
|
/>
|
||||||
|
<Menu.Separator/>
|
||||||
|
<UserAccountLogoutMenuItem/>
|
||||||
|
</Menu.Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useMemo} from 'react';
|
||||||
|
import type {MouseEvent, KeyboardEvent} from 'react';
|
||||||
|
import {defineMessages, useIntl} from 'react-intl';
|
||||||
|
|
||||||
|
import {CheckCircleIcon, ClockIcon, MinusCircleIcon, RadioboxBlankIcon} from '@mattermost/compass-icons/components';
|
||||||
|
|
||||||
|
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||||
|
import Avatar from 'components/widgets/users/avatar/avatar';
|
||||||
|
|
||||||
|
import {UserStatuses} from 'utils/constants';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
profilePicture?: string;
|
||||||
|
openCustomStatusModal: ((event: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>) => void);
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserAccountMenuButton({
|
||||||
|
profilePicture,
|
||||||
|
openCustomStatusModal,
|
||||||
|
status,
|
||||||
|
}: Props) {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
const statusIcon = useMemo(() => {
|
||||||
|
if (status && status === UserStatuses.ONLINE) {
|
||||||
|
return (
|
||||||
|
<CheckCircleIcon
|
||||||
|
size='16'
|
||||||
|
className='userAccountMenu_onlineMenuItem_icon'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status === UserStatuses.AWAY) {
|
||||||
|
return (
|
||||||
|
<ClockIcon
|
||||||
|
size='16'
|
||||||
|
className='userAccountMenu_awayMenuItem_icon'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status === UserStatuses.DND) {
|
||||||
|
return (
|
||||||
|
<MinusCircleIcon
|
||||||
|
size='16'
|
||||||
|
className='userAccountMenu_dndMenuItem_icon'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults to offline
|
||||||
|
return (
|
||||||
|
<RadioboxBlankIcon
|
||||||
|
size='16'
|
||||||
|
className='userAccountMenu_offlineMenuItem_icon'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CustomStatusEmoji
|
||||||
|
showTooltip={true}
|
||||||
|
emojiStyle={{marginRight: '6px'}}
|
||||||
|
aria-hidden={true}
|
||||||
|
onClick={openCustomStatusModal}
|
||||||
|
/>
|
||||||
|
<Avatar
|
||||||
|
size='sm'
|
||||||
|
url={profilePicture}
|
||||||
|
aria-hidden={true}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className='userStatusIconWrapper'
|
||||||
|
aria-hidden={true}
|
||||||
|
>
|
||||||
|
{statusIcon}
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
id='userAccountMenuButtonDescribedBy'
|
||||||
|
className='sr-only'
|
||||||
|
>
|
||||||
|
{formatMessage(getMenuButtonAriaDescription(status))}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ariaDescriptionsDefineMessages = defineMessages({
|
||||||
|
outOfOffice: {
|
||||||
|
id: 'userAccountMenu.menuButton.ariaDescription.ooo',
|
||||||
|
defaultMessage: 'Status is "Out of office".',
|
||||||
|
},
|
||||||
|
online: {
|
||||||
|
id: 'userAccountMenu.menuButton.ariaDescription.online',
|
||||||
|
defaultMessage: 'Status is "Online".',
|
||||||
|
},
|
||||||
|
away: {
|
||||||
|
id: 'userAccountMenu.menuButton.ariaDescription.away',
|
||||||
|
defaultMessage: 'Status is "Away".',
|
||||||
|
},
|
||||||
|
dnd: {
|
||||||
|
id: 'userAccountMenu.menuButton.ariaDescription.dnd',
|
||||||
|
defaultMessage: 'Status is "Do not disturb".',
|
||||||
|
},
|
||||||
|
offline: {
|
||||||
|
id: 'userAccountMenu.menuButton.ariaDescription.offline',
|
||||||
|
defaultMessage: 'Status is "Offline".',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function getMenuButtonAriaDescription(status?: string) {
|
||||||
|
let ariaLabel;
|
||||||
|
switch (status) {
|
||||||
|
case UserStatuses.OUT_OF_OFFICE:
|
||||||
|
ariaLabel = ariaDescriptionsDefineMessages.outOfOffice;
|
||||||
|
break;
|
||||||
|
case UserStatuses.ONLINE:
|
||||||
|
ariaLabel = ariaDescriptionsDefineMessages.online;
|
||||||
|
break;
|
||||||
|
case UserStatuses.AWAY:
|
||||||
|
ariaLabel = ariaDescriptionsDefineMessages.away;
|
||||||
|
break;
|
||||||
|
case UserStatuses.DND:
|
||||||
|
ariaLabel = ariaDescriptionsDefineMessages.dnd;
|
||||||
|
break;
|
||||||
|
case UserStatuses.OFFLINE:
|
||||||
|
default:
|
||||||
|
ariaLabel = ariaDescriptionsDefineMessages.offline;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ariaLabel;
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import * as reactRedux from 'react-redux';
|
||||||
|
|
||||||
|
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
|
||||||
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
|
import UserAccountNameMenuItem from './user_account_name_menuitem';
|
||||||
|
|
||||||
|
jest.mock('react-redux', () => ({
|
||||||
|
...jest.requireActual('react-redux') as typeof import('react-redux'),
|
||||||
|
useDispatch: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('actions/views/modals', () => ({
|
||||||
|
openModal: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('UserAccountNameMenuItem', () => {
|
||||||
|
const user = TestHelper.getUserMock({
|
||||||
|
first_name: 'sampleFirstName',
|
||||||
|
last_name: 'sampleLastName',
|
||||||
|
username: 'sampleUsername',
|
||||||
|
});
|
||||||
|
|
||||||
|
let initialState: GlobalState;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initialState = {
|
||||||
|
entities: {
|
||||||
|
users: {
|
||||||
|
profiles: {
|
||||||
|
[user.id]: user,
|
||||||
|
},
|
||||||
|
currentUserId: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as GlobalState;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render with both first and last name along with username', () => {
|
||||||
|
renderWithContext(<UserAccountNameMenuItem/>, initialState);
|
||||||
|
|
||||||
|
expect(screen.getByText('sampleFirstName sampleLastName')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('@sampleUsername')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render with only username', () => {
|
||||||
|
const state = {
|
||||||
|
entities: {
|
||||||
|
users: {
|
||||||
|
profiles: {
|
||||||
|
[user.id]: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentUserId: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as GlobalState;
|
||||||
|
|
||||||
|
renderWithContext(<UserAccountNameMenuItem/>, state);
|
||||||
|
|
||||||
|
expect(screen.queryByText('sampleFirstName sampleLastName')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText('@sampleUsername')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should try to open user settings modal', async () => {
|
||||||
|
jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(jest.fn());
|
||||||
|
|
||||||
|
renderWithContext(<UserAccountNameMenuItem/>, initialState);
|
||||||
|
|
||||||
|
userEvent.click(screen.getByRole('menuitem'));
|
||||||
|
|
||||||
|
expect(reactRedux.useDispatch).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not break if no props are passed', () => {
|
||||||
|
const {container} = renderWithContext(<UserAccountNameMenuItem/>);
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
|
import {openModal} from 'actions/views/modals';
|
||||||
|
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
import UserSettingsModal from 'components/user_settings/modal';
|
||||||
|
import Avatar from 'components/widgets/users/avatar/avatar';
|
||||||
|
|
||||||
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
|
|
||||||
|
interface Props extends Menu.FirstMenuItemProps {
|
||||||
|
profilePicture?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserAccountNameMenuItem({profilePicture, ...rest}: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const currentUser = useSelector(getCurrentUser);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.USER_SETTINGS,
|
||||||
|
dialogType: UserSettingsModal,
|
||||||
|
dialogProps: {isContentProductSettings: false},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabel() {
|
||||||
|
if (
|
||||||
|
currentUser?.first_name?.length > 0 ||
|
||||||
|
currentUser?.last_name?.length > 0
|
||||||
|
) {
|
||||||
|
const name = `${currentUser?.first_name} ${currentUser?.last_name}`?.trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className='userAccountMenu_nameMenuItem_primaryLabel'>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
<span className='userAccountMenu_nameMenuItem_secondaryLabel'>
|
||||||
|
{'@' + currentUser?.username}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = `@${currentUser?.username}`?.trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h2 className='userAccountMenu_nameMenuItem_primaryLabel'>
|
||||||
|
{username}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
className='userAccountMenu_nameMenuItem'
|
||||||
|
leadingElement={
|
||||||
|
<Avatar
|
||||||
|
size='lg'
|
||||||
|
url={profilePicture}
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labels={getLabel()}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-haspopup={true}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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, RadioboxBlankIcon} 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;
|
||||||
|
isStatusOffline: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserAccountOfflineMenuItem(props: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (props.shouldConfirmBeforeStatusChange) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.RESET_STATUS,
|
||||||
|
dialogType: ResetStatusModal,
|
||||||
|
dialogProps: {
|
||||||
|
newStatus: UserStatuses.OFFLINE,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(setStatus({
|
||||||
|
user_id: props.userId,
|
||||||
|
status: UserStatuses.OFFLINE,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trailingElement = useMemo(() => {
|
||||||
|
if (props.isStatusOffline) {
|
||||||
|
return (
|
||||||
|
<CheckIcon
|
||||||
|
size={16}
|
||||||
|
className='userAccountMenu_menuItemTrailingCheckIcon'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [props.isStatusOffline]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
leadingElement={
|
||||||
|
<RadioboxBlankIcon
|
||||||
|
size='18'
|
||||||
|
className='userAccountMenu_offlineMenuItem_icon'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.offlineMenuItem.label'
|
||||||
|
defaultMessage='Offline'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
trailingElements={trailingElement}
|
||||||
|
aria-checked={props.isStatusOffline}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 {CheckCircleIcon, CheckIcon} 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 {UserStatuses, ModalIdentifiers} from 'utils/constants';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
userId: UserProfile['id'];
|
||||||
|
shouldConfirmBeforeStatusChange: boolean;
|
||||||
|
isStatusOnline: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserAccountOnlineMenuItem(props: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (props.shouldConfirmBeforeStatusChange) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.RESET_STATUS,
|
||||||
|
dialogType: ResetStatusModal,
|
||||||
|
dialogProps: {
|
||||||
|
newStatus: UserStatuses.ONLINE,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(setStatus({
|
||||||
|
user_id: props.userId,
|
||||||
|
status: UserStatuses.ONLINE,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trailingElement = useMemo(() => {
|
||||||
|
if (props.isStatusOnline) {
|
||||||
|
return (
|
||||||
|
<CheckIcon
|
||||||
|
size={16}
|
||||||
|
className='userAccountMenu_menuItemTrailingCheckIcon'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [props.isStatusOnline]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
leadingElement={
|
||||||
|
<CheckCircleIcon
|
||||||
|
size='18'
|
||||||
|
className='userAccountMenu_onlineMenuItem_icon'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.onlineMenuItem.label'
|
||||||
|
defaultMessage='Online'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
trailingElements={trailingElement}
|
||||||
|
aria-checked={props.isStatusOnline}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import * as modalActions from 'actions/views/modals';
|
||||||
|
|
||||||
|
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
|
||||||
|
import {WindowSizes} from 'utils/constants';
|
||||||
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
|
import UserAccountOutOfOfficeMenuItem, {type Props} from './user_account_out_of_office_menuitem';
|
||||||
|
|
||||||
|
describe('UserAccountOutOfOfficeMenuItem', () => {
|
||||||
|
let defaultProps: Props;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
defaultProps = {
|
||||||
|
userId: TestHelper.getUserMock().id,
|
||||||
|
shouldConfirmBeforeStatusChange: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not render if status is not out of office', () => {
|
||||||
|
const props = {...defaultProps, isStatusOutOfOffice: false};
|
||||||
|
renderWithContext(<UserAccountOutOfOfficeMenuItem {...props}/>);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Out of office')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should only render when status is out of office', () => {
|
||||||
|
renderWithContext(<UserAccountOutOfOfficeMenuItem {...defaultProps}/>);
|
||||||
|
|
||||||
|
expect(screen.getAllByText(/Out of office/).length).toBe(1);
|
||||||
|
expect(screen.getAllByText(/Automatic replies are enabled/).length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should open reset status modal when confirming before status change option is true', async () => {
|
||||||
|
jest.spyOn(modalActions, 'openModal');
|
||||||
|
|
||||||
|
// Work around since in mobile menu's onClick get executed immediately and not so on non-mobile
|
||||||
|
// see handleClick func of webapp/channels/src/components/menu/menu_item.tsx
|
||||||
|
const initialState = {
|
||||||
|
views: {
|
||||||
|
browser: {
|
||||||
|
windowSize: WindowSizes.MOBILE_VIEW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const props = {...defaultProps, shouldConfirmBeforeStatusChange: true};
|
||||||
|
renderWithContext(<UserAccountOutOfOfficeMenuItem {...props}/>, initialState);
|
||||||
|
|
||||||
|
userEvent.click(screen.getByRole('menuitem'));
|
||||||
|
|
||||||
|
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {useDispatch} from 'react-redux';
|
||||||
|
|
||||||
|
import {CancelIcon, CheckIcon} 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 {UserStatuses, ModalIdentifiers} from 'utils/constants';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
userId: UserProfile['id'];
|
||||||
|
shouldConfirmBeforeStatusChange: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserAccountOutOfOfficeMenuItem(props: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (props.shouldConfirmBeforeStatusChange) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.RESET_STATUS,
|
||||||
|
dialogType: ResetStatusModal,
|
||||||
|
dialogProps: {
|
||||||
|
newStatus: UserStatuses.ONLINE,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
dispatch(setStatus({
|
||||||
|
user_id: props.userId,
|
||||||
|
status: UserStatuses.ONLINE,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
leadingElement={
|
||||||
|
<CancelIcon
|
||||||
|
size={18}
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labels={
|
||||||
|
<>
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.oooMenuItem.primaryLabel'
|
||||||
|
defaultMessage='Out of office'
|
||||||
|
/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.oooMenuItem.secondaryLabel'
|
||||||
|
defaultMessage='Automatic replies are enabled'
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
trailingElements={
|
||||||
|
<CheckIcon
|
||||||
|
size={16}
|
||||||
|
className='userAccountMenu_menuItemTrailingCheckIcon'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
aria-checked='true'
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {AccountOutlineIcon} from '@mattermost/compass-icons/components';
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
|
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||||
|
import {getInt} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
|
import {openModal} from 'actions/views/modals';
|
||||||
|
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
import {OnboardingTaskCategory, OnboardingTasksName, TaskNameMapToSteps, CompleteYourProfileTour} from 'components/onboarding_tasks';
|
||||||
|
import UserSettingsModal from 'components/user_settings/modal';
|
||||||
|
|
||||||
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
userId: UserProfile['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserAccountProfileMenuItem(props: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onboardingTaskStep = useSelector((state: GlobalState) => getInt(state, OnboardingTaskCategory, OnboardingTasksName.COMPLETE_YOUR_PROFILE, 0));
|
||||||
|
const isCompleteYourProfileTaskPending = onboardingTaskStep === TaskNameMapToSteps[OnboardingTasksName.COMPLETE_YOUR_PROFILE].STARTED;
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.USER_SETTINGS,
|
||||||
|
dialogType: UserSettingsModal,
|
||||||
|
dialogProps: {isContentProductSettings: false},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTourClick() {
|
||||||
|
const taskName = OnboardingTasksName.COMPLETE_YOUR_PROFILE;
|
||||||
|
const steps = TaskNameMapToSteps[taskName];
|
||||||
|
|
||||||
|
dispatch(savePreferences(props.userId, [{
|
||||||
|
user_id: props.userId,
|
||||||
|
category: OnboardingTaskCategory,
|
||||||
|
name: taskName,
|
||||||
|
value: steps.FINISHED.toString(),
|
||||||
|
}]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
leadingElement={
|
||||||
|
<AccountOutlineIcon
|
||||||
|
size={18}
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.profileMenuItem.label'
|
||||||
|
defaultMessage='Profile'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
trailingElements={isCompleteYourProfileTaskPending && (
|
||||||
|
<div
|
||||||
|
onClick={handleTourClick}
|
||||||
|
className='userAccountMenu_profileMenuItem_tourWrapper'
|
||||||
|
>
|
||||||
|
<CompleteYourProfileTour/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
aria-haspopup={true}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {MouseEvent, KeyboardEvent} from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
import EmojiIcon from 'components/widgets/icons/emoji_icon';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
openCustomStatusModal: (event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserAccountSetCustomStatusMenuItem(props: Props) {
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
className='userAccountMenu_setCustomStatusMenuItem'
|
||||||
|
leadingElement={
|
||||||
|
<EmojiIcon
|
||||||
|
className='userAccountMenu_setCustomStatusMenuItem_icon'
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='userAccountMenu.setCustomStatusMenuItem.noStatusSet'
|
||||||
|
defaultMessage='Set custom status'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
aria-haspopup={true}
|
||||||
|
onClick={props.openCustomStatusModal}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -55,7 +55,6 @@ const ViewUserGroupListItem = (props: Props) => {
|
|||||||
className='group-member-row'
|
className='group-member-row'
|
||||||
>
|
>
|
||||||
<span className='status-wrapper'>
|
<span className='status-wrapper'>
|
||||||
|
|
||||||
<Avatar
|
<Avatar
|
||||||
username={user.username}
|
username={user.username}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
|
|||||||
@@ -3556,7 +3556,6 @@
|
|||||||
"custom_status.expiry_dropdown.today": "Today",
|
"custom_status.expiry_dropdown.today": "Today",
|
||||||
"custom_status.expiry.time_picker.title": "Time",
|
"custom_status.expiry.time_picker.title": "Time",
|
||||||
"custom_status.expiry.until": "Until {time}",
|
"custom_status.expiry.until": "Until {time}",
|
||||||
"custom_status.expiry.until_tomorrow": "Until Tomorrow {time}",
|
|
||||||
"custom_status.modal_cancel": "Clear Status",
|
"custom_status.modal_cancel": "Clear Status",
|
||||||
"custom_status.modal_confirm": "Set Status",
|
"custom_status.modal_confirm": "Set Status",
|
||||||
"custom_status.set_status": "Set a status",
|
"custom_status.set_status": "Set a status",
|
||||||
@@ -5259,29 +5258,6 @@
|
|||||||
"start_trial.modal.loading": "Loading...",
|
"start_trial.modal.loading": "Loading...",
|
||||||
"start_trial.tutorialTip.desc": "Explore our most requested premium features. Determine user access with Guest Accounts, automate compliance reports, and send secure ID-only mobile push notifications.",
|
"start_trial.tutorialTip.desc": "Explore our most requested premium features. Determine user access with Guest Accounts, automate compliance reports, and send secure ID-only mobile push notifications.",
|
||||||
"start_trial.tutorialTip.title": "Try our premium features for free",
|
"start_trial.tutorialTip.title": "Try our premium features for free",
|
||||||
"status_dropdown.dnd_sub_menu_header": "Clear after:",
|
|
||||||
"status_dropdown.dnd_sub_menu_item.custom": "Choose date and time",
|
|
||||||
"status_dropdown.dnd_sub_menu_item.dont_clear": "Don't clear",
|
|
||||||
"status_dropdown.dnd_sub_menu_item.one_hour": "1 hour",
|
|
||||||
"status_dropdown.dnd_sub_menu_item.thirty_minutes": "30 mins",
|
|
||||||
"status_dropdown.dnd_sub_menu_item.tomorrow": "Tomorrow",
|
|
||||||
"status_dropdown.dnd_sub_menu_item.two_hours": "2 hours",
|
|
||||||
"status_dropdown.menuAriaLabel": "set status",
|
|
||||||
"status_dropdown.profile_button_label": "Select to open profile and status menu.",
|
|
||||||
"status_dropdown.profile_button_label.away": "Current status: Away. Select to open profile and status menu.",
|
|
||||||
"status_dropdown.profile_button_label.dnd": "Current status: Do not disturb. Select to open profile and status menu.",
|
|
||||||
"status_dropdown.profile_button_label.offline": "Current status: Offline. Select to open profile and status menu.",
|
|
||||||
"status_dropdown.profile_button_label.online": "Current status: Online. Select to open profile and status menu.",
|
|
||||||
"status_dropdown.profile_button_label.ooo": "Current status: Out of office. Select to open profile and status menu.",
|
|
||||||
"status_dropdown.set_away": "Away",
|
|
||||||
"status_dropdown.set_custom": "Set a custom status",
|
|
||||||
"status_dropdown.set_custom_text": "Set custom status text...",
|
|
||||||
"status_dropdown.set_dnd": "Do not disturb",
|
|
||||||
"status_dropdown.set_dnd.extra": "Disables all notifications",
|
|
||||||
"status_dropdown.set_offline": "Offline",
|
|
||||||
"status_dropdown.set_online": "Online",
|
|
||||||
"status_dropdown.set_ooo": "Out of office",
|
|
||||||
"status_dropdown.set_ooo.extra": "Automatic replies are enabled",
|
|
||||||
"success_modal.return_to_workspace": "Return to workspace",
|
"success_modal.return_to_workspace": "Return to workspace",
|
||||||
"success_modal.subtitle": "Your final bill will be prorated. Your workspace now has {plan} limits.",
|
"success_modal.subtitle": "Your final bill will be prorated. Your workspace now has {plan} limits.",
|
||||||
"suggestion.all": "All channel types",
|
"suggestion.all": "All channel types",
|
||||||
@@ -5935,6 +5911,39 @@
|
|||||||
"user.settings.tokens.tokenId": "Token ID: ",
|
"user.settings.tokens.tokenId": "Token ID: ",
|
||||||
"user.settings.tokens.tokenLoading": "Loading...",
|
"user.settings.tokens.tokenLoading": "Loading...",
|
||||||
"user.settings.tokens.userAccessTokensNone": "No personal access tokens.",
|
"user.settings.tokens.userAccessTokensNone": "No personal access tokens.",
|
||||||
|
"userAccountMenu.awayMenuItem.label": "Away",
|
||||||
|
"userAccountMenu.dndMenuItem.primaryLabel": "Do not disturb",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel": "Disables all notifications",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.doNotClear": "Until indefinitely",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.untilLaterSomeTime": "Until {time}",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.untilTodaySomeTime": "Until {time}",
|
||||||
|
"userAccountMenu.dndMenuItem.secondaryLabel.untilTomorrowSomeTime": "Until tomorrow {time}",
|
||||||
|
"userAccountMenu.dndSubMenu.title": "Clear after:",
|
||||||
|
"userAccountMenu.dndSubMenuItem.1Hour": "1 hour",
|
||||||
|
"userAccountMenu.dndSubMenuItem.2Hours": "2 hours",
|
||||||
|
"userAccountMenu.dndSubMenuItem.30Minutes": "30 mins",
|
||||||
|
"userAccountMenu.dndSubMenuItem.custom": "Choose date and time",
|
||||||
|
"userAccountMenu.dndSubMenuItem.doNotClear": "Don't clear",
|
||||||
|
"userAccountMenu.dndSubMenuItem.tomorrow": "Tomorrow",
|
||||||
|
"userAccountMenu.dndSubMenuItem.tomorrowsDateTime": "{shortDay}, {shortTime}",
|
||||||
|
"userAccountMenu.logoutMenuItem.label": "Log out",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.away": "Status is \"Away\".",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.dnd": "Status is \"Do not disturb\".",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.offline": "Status is \"Offline\".",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.online": "Status is \"Online\".",
|
||||||
|
"userAccountMenu.menuButton.ariaDescription.ooo": "Status is \"Out of office\".",
|
||||||
|
"userAccountMenu.menuButton.ariaLabel": "User's account menu",
|
||||||
|
"userAccountMenu.offlineMenuItem.label": "Offline",
|
||||||
|
"userAccountMenu.onlineMenuItem.label": "Online",
|
||||||
|
"userAccountMenu.oooMenuItem.primaryLabel": "Out of office",
|
||||||
|
"userAccountMenu.oooMenuItem.secondaryLabel": "Automatic replies are enabled",
|
||||||
|
"userAccountMenu.profileMenuItem.label": "Profile",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.clearTooltip": "Clear custom status",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.hasStatusWithExpiryAndNoText.ariaDescription": "Status expires at {time}. Set a custom status.",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.hasStatusWithTextAndExpiry.ariaDescription": "Status is \"{text}\" and expires at {time}. Set a custom status.",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.hasStatusWithTextAndNoExpiry.ariaDescription": "Status is \"{text}\". Set a custom status.",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.noStatusSet": "Set custom status",
|
||||||
|
"userAccountMenu.setCustomStatusMenuItem.noStatusTextSet": "Set custom status text",
|
||||||
"userGuideHelp.askTheCommunity": "Ask the community",
|
"userGuideHelp.askTheCommunity": "Ask the community",
|
||||||
"userGuideHelp.helpResources": "Help resources",
|
"userGuideHelp.helpResources": "Help resources",
|
||||||
"userGuideHelp.keyboardShortcuts": "Keyboard shortcuts",
|
"userGuideHelp.keyboardShortcuts": "Keyboard shortcuts",
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import rhs from './rhs';
|
|||||||
import rhsSuppressed from './rhs_suppressed';
|
import rhsSuppressed from './rhs_suppressed';
|
||||||
import search from './search';
|
import search from './search';
|
||||||
import settings from './settings';
|
import settings from './settings';
|
||||||
import statusDropdown from './status_dropdown';
|
|
||||||
import system from './system';
|
import system from './system';
|
||||||
import textbox from './textbox';
|
import textbox from './textbox';
|
||||||
import threads from './threads';
|
import threads from './threads';
|
||||||
@@ -50,7 +49,6 @@ export default combineReducers({
|
|||||||
marketplace,
|
marketplace,
|
||||||
textbox,
|
textbox,
|
||||||
channelSidebar,
|
channelSidebar,
|
||||||
statusDropdown,
|
|
||||||
addChannelDropdown,
|
addChannelDropdown,
|
||||||
addChannelCtaDropdown,
|
addChannelCtaDropdown,
|
||||||
onboardingTasks,
|
onboardingTasks,
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import {combineReducers} from 'redux';
|
|
||||||
|
|
||||||
import {UserTypes} from 'mattermost-redux/action_types';
|
|
||||||
|
|
||||||
import {ActionTypes} from 'utils/constants';
|
|
||||||
|
|
||||||
import type {MMAction} from 'types/store';
|
|
||||||
|
|
||||||
export function isOpen(state = false, action: MMAction) {
|
|
||||||
switch (action.type) {
|
|
||||||
case ActionTypes.STATUS_DROPDOWN_TOGGLE:
|
|
||||||
return action.open;
|
|
||||||
|
|
||||||
case UserTypes.LOGOUT_SUCCESS:
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default combineReducers({
|
|
||||||
isOpen,
|
|
||||||
});
|
|
||||||
@@ -41,5 +41,8 @@ $elevation-6: var(--elevation-6);
|
|||||||
// Central place to manage z-index values for the app, providing a consistent way to manage stacking order.
|
// Central place to manage z-index values for the app, providing a consistent way to manage stacking order.
|
||||||
$z-index-popover: 1070;
|
$z-index-popover: 1070;
|
||||||
$z-index-user-group-popover: 1060;
|
$z-index-user-group-popover: 1060;
|
||||||
|
$z-index-tour-tips: 1058;
|
||||||
|
$z-index-menu: 1054;
|
||||||
|
$z-index-modal: 1050;
|
||||||
$z-index-global-header: 99;
|
$z-index-global-header: 99;
|
||||||
$z-index-channel-header: 15;
|
$z-index-channel-header: 15;
|
||||||
|
|||||||
@@ -1,19 +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 selector', () => {
|
|
||||||
it('should return the isOpen value from the state', async () => {
|
|
||||||
const store = await configureStore();
|
|
||||||
expect(isStatusDropdownOpen(store.getState())).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true if statusDropdown in explicitly opened', async () => {
|
|
||||||
const store = await configureStore();
|
|
||||||
await store.dispatch(setStatusDropdown(true));
|
|
||||||
expect(isStatusDropdownOpen(store.getState())).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
|
||||||
|
|
||||||
export function isStatusDropdownOpen(state: GlobalState) {
|
|
||||||
return state.views.statusDropdown.isOpen;
|
|
||||||
}
|
|
||||||
@@ -194,10 +194,6 @@ export type ViewsState = {
|
|||||||
lastSelectedChannel: string;
|
lastSelectedChannel: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
statusDropdown: {
|
|
||||||
isOpen: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
addChannelDropdown: {
|
addChannelDropdown: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -250,7 +250,6 @@ export const ActionTypes = keyMirror({
|
|||||||
INCREMENT_EMOJI_PICKER_PAGE: null,
|
INCREMENT_EMOJI_PICKER_PAGE: null,
|
||||||
SET_RECENT_SKIN: null,
|
SET_RECENT_SKIN: null,
|
||||||
|
|
||||||
STATUS_DROPDOWN_TOGGLE: null,
|
|
||||||
ADD_CHANNEL_DROPDOWN_TOGGLE: null,
|
ADD_CHANNEL_DROPDOWN_TOGGLE: null,
|
||||||
ADD_CHANNEL_CTA_DROPDOWN_TOGGLE: null,
|
ADD_CHANNEL_CTA_DROPDOWN_TOGGLE: null,
|
||||||
|
|
||||||
|
|||||||
@@ -33,3 +33,20 @@ export function isKeyPressed(event: React.KeyboardEvent | KeyboardEvent, key: [s
|
|||||||
// used for different language keyboards to detect the position of keys
|
// used for different language keyboards to detect the position of keys
|
||||||
return event.keyCode === key[1];
|
return event.keyCode === key[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MenuButtonIds = {
|
||||||
|
userAccountMenu: 'userAccountMenuButton',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since the menu component requires actual interaction with the button
|
||||||
|
* of the menu for opening the menus, we trigger it from here
|
||||||
|
*/
|
||||||
|
export function clickOnMenuButton(buttonId: string) {
|
||||||
|
const menuButton = document.getElementById(buttonId);
|
||||||
|
if (!menuButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuButton.click();
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,3 +35,37 @@
|
|||||||
animation: pulse2 2s ease 0s infinite;
|
animation: pulse2 2s ease 0s infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user