[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', () => {
|
||||
// * Verify image alt in profile image
|
||||
cy.uiGetProfileHeader().
|
||||
find('.Avatar').
|
||||
should('have.attr', 'alt', 'user profile image');
|
||||
cy.get('#userAccountMenuButton').within(() => {
|
||||
cy.findByAltText('user profile image').should('be.visible');
|
||||
});
|
||||
|
||||
// # Upload an image in the post
|
||||
cy.get('#fileUploadInput').attachFile('small-image.png');
|
||||
|
||||
@@ -163,9 +163,6 @@ describe('Profile > Profile Settings > Email', () => {
|
||||
// # Click on the link
|
||||
cy.visit(permalink);
|
||||
|
||||
// * Verify announcement bar
|
||||
cy.get('.announcement-bar').should('be.visible').should('contain.text', 'Email verified');
|
||||
|
||||
// # Wait for one second for the mail to be sent out.
|
||||
cy.wait(TIMEOUTS.FIVE_SEC);
|
||||
|
||||
@@ -199,7 +196,7 @@ describe('Profile > Profile Settings > Email', () => {
|
||||
|
||||
// # Close modal then logout
|
||||
cy.get('body').type('{esc}');
|
||||
cy.uiOpenUserMenu('Log Out');
|
||||
cy.uiLogout();
|
||||
|
||||
// # Wait for one second for the mail to be sent out.
|
||||
cy.wait(TIMEOUTS.ONE_SEC);
|
||||
|
||||
@@ -116,8 +116,8 @@ describe('Settings > Sidebar > General > Edit', () => {
|
||||
// # Step 3
|
||||
// * Verify that we've logged in as the temp user
|
||||
cy.visit(offTopicUrl);
|
||||
cy.uiOpenUserMenu().findByText(`@${newTempUserName}`);
|
||||
cy.uiGetSetStatusButton().click();
|
||||
cy.uiOpenUserMenu().findByText(`@${newTempUserName}`).should('exist');
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
// # Step 4
|
||||
const text = `${newTempUserName} test message!`;
|
||||
|
||||
@@ -124,5 +124,5 @@ function verifyUsername(username) {
|
||||
cy.uiOpenUserMenu().findByText(`@${username}`);
|
||||
|
||||
// # 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.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as TIMEOUTS from '../../../fixtures/timeouts';
|
||||
|
||||
import {clickCategoryMenuItem} from './helpers';
|
||||
|
||||
// ***************************************************************
|
||||
@@ -22,6 +24,8 @@ describe('Sidebar category menu', () => {
|
||||
it('MM-T3171_1 Verify that the 3-dot menu on the Channels Category contains an option to Create New Category', () => {
|
||||
clickCategoryMenuItem('CHANNELS', 'Create New Category');
|
||||
|
||||
cy.wait(TIMEOUTS.ONE_SEC);
|
||||
|
||||
cy.get('body').type('{esc}', {force: true});
|
||||
});
|
||||
|
||||
@@ -41,6 +45,8 @@ describe('Sidebar category menu', () => {
|
||||
// # Verify that Create New Category exists on Favorites category and click on it
|
||||
clickCategoryMenuItem('FAVORITES', 'Create New Category');
|
||||
|
||||
cy.wait(TIMEOUTS.ONE_SEC);
|
||||
|
||||
cy.get('body').type('{esc}', {force: true});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,15 +28,23 @@ describe('Custom Status - Setting a Custom Status', () => {
|
||||
|
||||
it('MM-T3836_1 should open status dropdown', () => {
|
||||
// # Click on the sidebar header to open status dropdown
|
||||
cy.get('.MenuWrapper .status-wrapper').click();
|
||||
cy.uiGetSetStatusButton().click();
|
||||
|
||||
// * Check if the status dropdown opens
|
||||
cy.get('#statusDropdownMenu').should('exist');
|
||||
cy.uiGetStatusMenuContainer();
|
||||
|
||||
// # Close the status dropdown
|
||||
cy.get('body').type('{esc}');
|
||||
});
|
||||
|
||||
it('MM-T3836_2 Custom status modal opens with 5 default statuses listed', () => {
|
||||
// # Click on the sidebar header to open status dropdown
|
||||
cy.uiGetSetStatusButton().click();
|
||||
|
||||
// * Check if the status dropdown opens
|
||||
cy.uiGetStatusMenuContainer().findByText('Set custom status').click();
|
||||
|
||||
// # Open custom status modal
|
||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
||||
cy.get('#custom_status_modal').should('exist');
|
||||
|
||||
// * Check if all the default suggestions exist
|
||||
@@ -111,13 +119,15 @@ describe('Custom Status - Setting a Custom Status', () => {
|
||||
trigger('mouseover');
|
||||
|
||||
// * Custom status tooltip should be visible
|
||||
cy.get('#custom-status-tooltip').should('exist');
|
||||
cy.get('#custom-status-tooltip').should('exist').and('be.visible');
|
||||
|
||||
// * Tooltip should contain the correct custom status emoji
|
||||
cy.get('#custom-status-tooltip .custom-status span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji);
|
||||
cy.get('#custom-status-tooltip').within(() => {
|
||||
// * Tooltip should contain the correct custom status emoji
|
||||
cy.get(`span[data-emoticon="${customStatus.emoji}"]`).should('exist');
|
||||
|
||||
// * Tooltip should contain the correct custom status text
|
||||
cy.get('#custom-status-tooltip .custom-status span.custom-status-text').should('have.text', customStatus.text);
|
||||
// * Tooltip should contain the correct custom status text
|
||||
cy.findByText(customStatus.text).should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T3836_8 should open custom status modal when emoji in LHS header is clicked', () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('Custom Status - Setting Your Own Custom Status', () => {
|
||||
|
||||
it('MM-T3846_1 should change the emoji to speech balloon when typed in the input', () => {
|
||||
// # Open the custom status modal
|
||||
cy.uiOpenUserMenu('Set a custom status');
|
||||
cy.uiOpenUserMenu('Set custom status');
|
||||
|
||||
// * Default emoji is currently visible in the custom status input
|
||||
cy.get('#custom_status_modal .StatusModal__emoji-button span').should('have.class', 'icon--emoji');
|
||||
@@ -72,20 +72,26 @@ describe('Custom Status - Setting Your Own Custom Status', () => {
|
||||
cy.uiGetProfileHeader().
|
||||
find('.emoticon').
|
||||
should('have.attr', 'data-emoticon', customStatus.emoji);
|
||||
|
||||
cy.get('body').type('{esc}');
|
||||
});
|
||||
|
||||
it('MM-T3846_5 should show custom status with emoji in the status dropdown', () => {
|
||||
// # Open user menu
|
||||
cy.uiOpenUserMenu();
|
||||
cy.uiOpenUserMenu().within(() => {
|
||||
// * Correct custom status text and emoji should be displayed in the status dropdown
|
||||
cy.findByText(customStatus.text).should('exist');
|
||||
cy.get(`span[data-emoticon="${customStatus.emoji}"]`).should('exist');
|
||||
});
|
||||
|
||||
// * Correct custom status text and emoji should be displayed in the status dropdown
|
||||
cy.get('.status-dropdown-menu .custom_status__container').should('have.text', customStatus.text);
|
||||
cy.get('.status-dropdown-menu .custom_status__row span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji);
|
||||
cy.get('body').type('{esc}');
|
||||
});
|
||||
|
||||
it('MM-T3846_6 should show previosly set status in the first position in Recents list', () => {
|
||||
it('MM-T3846_6 should show previously set status in the first position in Recents list', () => {
|
||||
// # Click on the "Set a Custom Status" option in the status dropdown
|
||||
cy.get('.status-dropdown-menu li#status-menu-custom-status').click();
|
||||
cy.uiOpenUserMenu().within(() => {
|
||||
cy.findByText(customStatus.text).should('exist').click({force: true});
|
||||
});
|
||||
|
||||
// * Custom status modal should open
|
||||
cy.get('#custom_status_modal').should('exist');
|
||||
@@ -118,9 +124,9 @@ describe('Custom Status - Setting Your Own Custom Status', () => {
|
||||
cy.findByText('Clear Status').click();
|
||||
|
||||
// # Open user menu
|
||||
cy.uiOpenUserMenu();
|
||||
|
||||
// * Custom status text should not be displayed in the status dropdown
|
||||
cy.get('.status-dropdown-menu .custom_status__row').should('not.have.text', customStatus.text);
|
||||
cy.uiOpenUserMenu().within(() => {
|
||||
// * Custom status text should not be displayed in the status dropdown
|
||||
cy.findByText(customStatus.text).should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('Custom Status - Recent Statuses', () => {
|
||||
|
||||
it('MM-T3847_1 set a status', () => {
|
||||
// # Open the custom status modal
|
||||
cy.uiOpenUserMenu('Set a custom status');
|
||||
cy.uiOpenUserMenu('Set custom status');
|
||||
|
||||
// # Type the custom status text in the custom status modal input
|
||||
cy.get('#custom_status_modal .StatusModal__input input').typeWithForce(customStatus.text);
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('Custom Status - Verifying Where Custom Status Appears', () => {
|
||||
});
|
||||
|
||||
it('MM-T3850_1 set a status', () => {
|
||||
cy.uiOpenUserMenu('Set a Custom Status');
|
||||
cy.uiOpenUserMenu('Set custom status');
|
||||
|
||||
// # Type the custom status text in the custom status modal input
|
||||
cy.findByPlaceholderText('Set a status').type(customStatus.text, {force: true});
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('MM-T4063 Custom status expiry', () => {
|
||||
const defaultCustomStatuses = ['In a meeting', 'Out for lunch', 'Out sick', 'Working from home', 'On a vacation'];
|
||||
const customStatus = {
|
||||
emoji: 'hamburger',
|
||||
emojiAriaLabel: ':hamburger:',
|
||||
text: 'Out for lunch',
|
||||
duration: '30 minutes',
|
||||
};
|
||||
@@ -36,19 +37,25 @@ describe('MM-T4063 Custom status expiry', () => {
|
||||
const expiryTimeFormat = 'h:mm A';
|
||||
it('MM-T4063_1 should open status dropdown', () => {
|
||||
// # Click on the sidebar header to open status dropdown
|
||||
cy.get('.MenuWrapper .status-wrapper').click();
|
||||
cy.uiGetSetStatusButton().click();
|
||||
|
||||
// * Check if the status dropdown opens
|
||||
cy.get('#statusDropdownMenu').should('exist');
|
||||
cy.uiGetStatusMenu();
|
||||
|
||||
// # Close the status dropdown
|
||||
cy.get('body').click();
|
||||
});
|
||||
|
||||
it('MM-T4063_2 Custom status modal opens with 5 default statuses listed', () => {
|
||||
// # Open custom status modal
|
||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
||||
cy.get('#custom_status_modal').should('exist');
|
||||
cy.uiOpenUserMenu('Set custom status');
|
||||
|
||||
// * Check if all the default suggestions exist
|
||||
defaultCustomStatuses.map((statusText) => cy.get('#custom_status_modal .statusSuggestion__content').contains('span', statusText));
|
||||
cy.findByRole('dialog', {name: 'Set a status'}).should('exist').within(() => {
|
||||
// * Check if all the default suggestions exist
|
||||
defaultCustomStatuses.forEach((statusText) => {
|
||||
cy.findByText(statusText).should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T4063_3 Correct custom status is selected with the correct emoji and correct duration', () => {
|
||||
@@ -94,17 +101,17 @@ describe('MM-T4063 Custom status expiry', () => {
|
||||
|
||||
it('MM-T4063_5 should show the set custom status with expiry when status dropdown is opened', () => {
|
||||
// # Click on the sidebar header to open status dropdown
|
||||
cy.get('.MenuWrapper .status-wrapper').click();
|
||||
cy.uiGetSetStatusButton().click();
|
||||
|
||||
// * Check if the status dropdown opens
|
||||
cy.get('#statusDropdownMenu').should('exist');
|
||||
cy.uiGetStatusMenu().within(() => {
|
||||
// * Correct custom status text and emoji should be displayed in the status dropdown
|
||||
cy.findByText(customStatus.text).should('exist');
|
||||
cy.findByLabelText(customStatus.emojiAriaLabel).should('exist');
|
||||
|
||||
// * Correct custom status text and emoji should be displayed in the status dropdown
|
||||
cy.get('.status-dropdown-menu .custom_status__container').should('have.text', customStatus.text);
|
||||
cy.get('.status-dropdown-menu .custom_status__row span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji);
|
||||
|
||||
// * Correct clear time should be displayed in the status dropdown
|
||||
cy.get('.status-dropdown-menu .custom_status__expiry time').invoke('text').should('match', expiresAtRegexp);
|
||||
// * Correct clear time should be displayed in the status dropdown
|
||||
cy.findByText(expiresAtRegexp).should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T4063_6 custom status should be cleared after duration of set custom status', () => {
|
||||
@@ -118,8 +125,7 @@ describe('MM-T4063 Custom status expiry', () => {
|
||||
|
||||
it('MM-T4063_7 current custom status should display expiry time in custom status modal', () => {
|
||||
// # Open custom status modal
|
||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
||||
cy.get('#custom_status_modal').should('exist');
|
||||
cy.get('.userAccountMenu_customStatusMenuItem').should('be.visible').click();
|
||||
|
||||
// * Should show expiry time of status when current status is selected
|
||||
cy.get('#custom_status_modal .statusSuggestion__content').contains('span', customStatus.text).click();
|
||||
@@ -132,15 +138,12 @@ describe('MM-T4063 Custom status expiry', () => {
|
||||
it('MM-T4063_8 previous custom status duration should be reset if custom status is expired', () => {
|
||||
// # Forwarding the time by the duration of custom status
|
||||
cy.clock(Date.now());
|
||||
cy.tick(waitingTime * 60 * 1000);
|
||||
|
||||
// # Open custom status modal
|
||||
cy.get('.status-dropdown-menu').click();
|
||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
||||
cy.get('#custom_status_modal').should('exist');
|
||||
|
||||
// * Should show duration of previous status when previous status is selected
|
||||
cy.get('#custom_status_modal .statusSuggestion__content').contains('span', customStatus.text).click();
|
||||
cy.get('#custom_status_modal .expiry-value').should('have.text', customStatus.duration);
|
||||
cy.tick(waitingTime * 60 * 1000).then(() => {
|
||||
// # Open custom status modal
|
||||
cy.uiOpenUserMenu().within(() => {
|
||||
// * Verify that there is no custom status in the dropdown
|
||||
cy.findByText('Set custom status').should('exist').and('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,86 +21,30 @@ describe('MM-T4064 Status expiry visibility', () => {
|
||||
cy.visit(channelUrl);
|
||||
});
|
||||
});
|
||||
|
||||
const defaultCustomStatuses = ['In a meeting', 'Out for lunch', 'Out sick', 'Working from home', 'On a vacation'];
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: '1 hour',
|
||||
};
|
||||
|
||||
const waitingTime = 60; //minutes
|
||||
let expiresAt = dayjs();
|
||||
const expiryTimeFormat = 'h:mm A';
|
||||
it('MM-T4064_1 should open status dropdown', () => {
|
||||
// # Click on the sidebar header to open status dropdown
|
||||
cy.get('.MenuWrapper .status-wrapper').click();
|
||||
|
||||
// * Check if the status dropdown opens
|
||||
cy.get('#statusDropdownMenu').should('exist');
|
||||
});
|
||||
it('MM-T4064_6 should show expiry time in the tooltip of custom status emoji in the post header', () => {
|
||||
// # Open the user account menu
|
||||
cy.uiOpenUserMenu('Set custom status');
|
||||
|
||||
it('MM-T4064_2 Custom status modal opens with 5 default statuses listed', () => {
|
||||
// # Open custom status modal
|
||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
||||
cy.get('#custom_status_modal').should('exist');
|
||||
// * Verify that the custom status modal opens
|
||||
cy.findByRole('dialog', {name: 'Set a status'}).should('exist').within(() => {
|
||||
// # Select a custom status from the suggestions
|
||||
cy.get('.statusSuggestion__row').first().click();
|
||||
|
||||
// * Check if all the default suggestions exist
|
||||
defaultCustomStatuses.map((statusText) => cy.get('#custom_status_modal .statusSuggestion__content').contains('span', statusText));
|
||||
});
|
||||
|
||||
it('MM-T4064_3 Correct custom status is selected with the correct emoji and correct duration', () => {
|
||||
// * Default emoji is currently visible in the custom status input
|
||||
cy.get('#custom_status_modal .StatusModal__emoji-button span').should('have.class', 'icon--emoji');
|
||||
|
||||
// * Input should be empty
|
||||
cy.get('#custom_status_modal input.form-control').should('have.value', '');
|
||||
|
||||
// # Select a custom status from the suggestions
|
||||
cy.get('#custom_status_modal .statusSuggestion__content').contains('span', customStatus.text).click();
|
||||
|
||||
// * Emoji in the custom status input should be changed
|
||||
cy.get('#custom_status_modal .StatusModal__emoji-button span').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji);
|
||||
|
||||
// * Selected custom status text should be in the input
|
||||
cy.get('#custom_status_modal input.form-control').should('have.value', customStatus.text);
|
||||
|
||||
// * Selected custom status duration should be displayed in the Clear after section
|
||||
cy.get('#custom_status_modal .expiry-wrapper .expiry-value').should('have.text', customStatus.duration);
|
||||
});
|
||||
|
||||
it('MM-T4064_4 should set custom status when click on Set Status', () => {
|
||||
// # Click on the Set Status button
|
||||
cy.get('#custom_status_modal .GenericModal__button.confirm').click();
|
||||
// # Click on the Set Status button
|
||||
cy.findByText('Set Status').click();
|
||||
});
|
||||
|
||||
// * Modal should be closed
|
||||
cy.get('#custom_status_modal').should('not.exist');
|
||||
|
||||
// # Setting the time at which the custom status should be expired
|
||||
// # Note that we need to be flexible around accepted values, as this calculation and the server-side one may differ slightly
|
||||
expiresAt = dayjs().add(waitingTime, 'minute');
|
||||
|
||||
// * Status should be set and the emoji should be visible in the sidebar header
|
||||
cy.uiGetProfileHeader().
|
||||
find('.emoticon').
|
||||
should('have.attr', 'data-emoticon', customStatus.emoji);
|
||||
});
|
||||
|
||||
it('MM-T4064_5 should show the set custom status with expiry when status dropdown is opened', () => {
|
||||
// # Click on the sidebar header to open status dropdown
|
||||
cy.get('.MenuWrapper .status-wrapper').click();
|
||||
|
||||
// * Check if the status dropdown opens
|
||||
cy.get('#statusDropdownMenu').should('exist');
|
||||
|
||||
// * Correct custom status text and emoji should be displayed in the status dropdown
|
||||
cy.get('.status-dropdown-menu .custom_status__container').should('have.text', customStatus.text);
|
||||
cy.get('.status-dropdown-menu .custom_status__row span.emoticon').invoke('attr', 'data-emoticon').should('contain', customStatus.emoji);
|
||||
|
||||
// * Correct clear time should be displayed in the status dropdown
|
||||
cy.get('.status-dropdown-menu .custom_status__expiry time').should('have.text', expiresAt.format(expiryTimeFormat));
|
||||
});
|
||||
|
||||
it('MM-T4064_6 should show expiry time in the tooltip of custom status emoji in the post header', () => {
|
||||
// # Post a message in the channel
|
||||
cy.postMessage('Hello World!');
|
||||
|
||||
|
||||
@@ -39,15 +39,15 @@ describe('MM-T4065 Setting manual status clear time less than 7 days away', () =
|
||||
|
||||
it('MM-T4065_1 should open status dropdown', () => {
|
||||
// # Click on the sidebar header to open status dropdown
|
||||
cy.get('.MenuWrapper .status-wrapper').click();
|
||||
cy.uiGetSetStatusButton().click();
|
||||
|
||||
// * Check if the status dropdown opens
|
||||
cy.get('#statusDropdownMenu').should('exist');
|
||||
cy.get('#userAccountMenu').should('exist');
|
||||
});
|
||||
|
||||
it('MM-T4065_2 Custom status modal opens with 5 default statuses listed', () => {
|
||||
// # Open custom status modal
|
||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
||||
cy.get('.userAccountMenu_setCustomStatusMenuItem').click();
|
||||
cy.get('#custom_status_modal').should('exist');
|
||||
|
||||
// * Check if all the default suggestions exist
|
||||
|
||||
@@ -38,15 +38,15 @@ describe('MM-T4066 Setting manual status clear time more than 7 days away', () =
|
||||
const months = dateToBeSelected.get('month') - today.get('month');
|
||||
it('MM-T4066_1 should open status dropdown', () => {
|
||||
// # Click on the sidebar header to open status dropdown
|
||||
cy.get('.MenuWrapper .status-wrapper').click();
|
||||
cy.uiGetSetStatusButton().click();
|
||||
|
||||
// * Check if the status dropdown opens
|
||||
cy.get('#statusDropdownMenu').should('exist');
|
||||
cy.get('#userAccountMenu').should('exist');
|
||||
});
|
||||
|
||||
it('MM-T4066_2 Custom status modal opens with 5 default statuses listed', () => {
|
||||
// # Open custom status modal
|
||||
cy.get('#statusDropdownMenu li#status-menu-custom-status').click();
|
||||
cy.get('.userAccountMenu_setCustomStatusMenuItem').click();
|
||||
cy.get('#custom_status_modal').should('exist');
|
||||
|
||||
// * Check if all the default suggestions exist
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
|
||||
describe('Integrations', () => {
|
||||
const testCases = [
|
||||
{command: '/away', className: 'icon-clock', message: 'You are now away'},
|
||||
{command: '/dnd', className: 'icon-minus-circle', message: 'Do Not Disturb is enabled. You will not receive desktop or mobile push notifications until Do Not Disturb is turned off.'},
|
||||
{command: '/offline', className: 'icon-circle-outline', message: 'You are now offline'},
|
||||
{command: '/online', className: 'icon-check-circle', message: 'You are now online'},
|
||||
{command: '/away', className: 'userAccountMenu_awayMenuItem_icon', message: 'You are now away'},
|
||||
{command: '/dnd', className: 'userAccountMenu_dndMenuItem_icon', message: 'Do Not Disturb is enabled. You will not receive desktop or mobile push notifications until Do Not Disturb is turned off.'},
|
||||
{command: '/offline', className: 'userAccountMenu_offlineMenuItem_icon', message: 'You are now offline'},
|
||||
{command: '/online', className: 'userAccountMenu_onlineMenuItem_icon', message: 'You are now online'},
|
||||
];
|
||||
|
||||
let offTopicUrl;
|
||||
@@ -67,7 +67,7 @@ describe('Integrations', () => {
|
||||
function verifyUserStatus(testCase, isCompactMode) {
|
||||
// * Verify that the user status is as indicated
|
||||
cy.uiGetProfileHeader().
|
||||
find('i').
|
||||
find('svg').
|
||||
should('be.visible').
|
||||
and('have.class', testCase.className);
|
||||
|
||||
|
||||
@@ -26,14 +26,14 @@ describe('Integrations', () => {
|
||||
|
||||
it('MM-T670 /away', () => {
|
||||
// # Set online status and verify it's changed as the initial status
|
||||
setStatus(online.name, online.profileClassName);
|
||||
setStatus(online.name);
|
||||
|
||||
verifyUserStatus(away);
|
||||
});
|
||||
|
||||
it('MM-T672 /offline', () => {
|
||||
// # Set online status and verify it's changed as the initial status
|
||||
setStatus(online.name, online.profileClassName);
|
||||
setStatus(online.name);
|
||||
|
||||
verifyUserStatus(offline);
|
||||
|
||||
@@ -60,17 +60,14 @@ describe('Integrations', () => {
|
||||
|
||||
it('MM-T674 /online', () => {
|
||||
// # Set offline status and verify it's changed as the initial status
|
||||
setStatus(offline.name, offline.className);
|
||||
setStatus(offline.name);
|
||||
|
||||
verifyUserStatus(online);
|
||||
});
|
||||
});
|
||||
|
||||
function setStatus(status, icon) {
|
||||
function setStatus(status) {
|
||||
cy.apiUpdateUserStatus(status);
|
||||
cy.uiGetProfileHeader().
|
||||
find('i').
|
||||
and('have.class', icon);
|
||||
}
|
||||
|
||||
function verifyUserStatus(testCase) {
|
||||
@@ -90,9 +87,7 @@ function verifyUserStatus(testCase) {
|
||||
});
|
||||
|
||||
// * Verify status shown at user profile in LHS
|
||||
cy.uiGetProfileHeader().
|
||||
find('i').
|
||||
and('have.class', testCase.profileClassName || testCase.className);
|
||||
cy.get('#userAccountMenuButtonDescribedBy').should('exist').and('include.text', `Status is "${testCase.name.charAt(0).toUpperCase() + testCase.name.slice(1)}"`);
|
||||
|
||||
// # Post a message
|
||||
cy.postMessage(testCase.name);
|
||||
|
||||
@@ -33,10 +33,9 @@ describe('Main menu', () => {
|
||||
cy.apiLogin(testUser);
|
||||
cy.visit(`/${testTeam.name}/channels/town-square`);
|
||||
|
||||
cy.uiOpenUserMenu();
|
||||
|
||||
cy.findByRole('menu').should('exist').find('.menu-divider:visible').last().click();
|
||||
cy.findByRole('menu').should('exist');
|
||||
cy.uiOpenUserMenu().within(() => {
|
||||
cy.findAllByRole('separator').last().click();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show integrations option for system administrator', () => {
|
||||
|
||||
@@ -10,15 +10,14 @@
|
||||
// Stage: @prod
|
||||
// Group: @channels @menu @custom_status @status_menu
|
||||
|
||||
import * as TIMEOUTS from '../../../fixtures/timeouts';
|
||||
import theme from '../../../fixtures/theme.json';
|
||||
|
||||
describe('Status dropdown menu', () => {
|
||||
const statusTestCases = [
|
||||
{text: 'Online', className: 'icon-check-circle', profileClassName: 'icon-check-circle'},
|
||||
{text: 'Away', className: 'icon-clock'},
|
||||
{text: 'Do not disturb', className: 'icon-minus-circle'},
|
||||
{text: 'Offline', className: 'icon-circle-outline'},
|
||||
{text: 'Online', className: 'userAccountMenu_onlineMenuItem_icon'},
|
||||
{text: 'Away', className: 'userAccountMenu_awayMenuItem_icon'},
|
||||
{text: 'Do not disturb', className: 'userAccountMenu_dndMenuItem_icon'},
|
||||
{text: 'Offline', className: 'userAccountMenu_offlineMenuItem_icon'},
|
||||
];
|
||||
|
||||
before(() => {
|
||||
@@ -40,9 +39,10 @@ describe('Status dropdown menu', () => {
|
||||
|
||||
// * Verify all available statuses are shown with icon and text
|
||||
statusTestCases.forEach((tc) => {
|
||||
cy.get('@userMenu').findByText(tc.text).
|
||||
parent().
|
||||
find('i').should('have.class', tc.className);
|
||||
cy.uiGetStatusMenu().within(() => {
|
||||
cy.findByText(tc.text).should('be.visible');
|
||||
cy.get('svg').filter(`.${tc.className}`).should('have.length', 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('Status dropdown menu', () => {
|
||||
cy.uiOpenUserMenu().as('userMenu');
|
||||
|
||||
// * Verify "Set a Custom Status" header is clickable
|
||||
cy.get('@userMenu').findByText('Set a custom status').should('have.css', 'cursor', 'pointer');
|
||||
cy.get('@userMenu').findByText('Set custom status').should('have.css', 'cursor', 'pointer');
|
||||
});
|
||||
|
||||
it('MM-T2927_5 When custom status is disabled, status menu is displayed when status icon is clicked', () => {
|
||||
@@ -81,22 +81,12 @@ describe('Status dropdown menu', () => {
|
||||
cy.uiOpenUserMenu();
|
||||
});
|
||||
|
||||
it('MM-T4420 Should stay open when dnd sub-menu header is clicked', () => {
|
||||
// # Open Dnd sub menu and click on header
|
||||
cy.uiOpenDndStatusSubMenu().find('#dndSubMenu-header_menuitem').click().then(() => {
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// * Verify that dnd submenu is still visible
|
||||
cy.get('body').find('#dndSubMenu-header_menuitem').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('MM-T4914 Profile menu header is clickable, opens Profile settings', () => {
|
||||
// # Open user menu
|
||||
cy.uiOpenUserMenu().as('userMenu');
|
||||
|
||||
// * Verify menu header is clickable
|
||||
cy.get('@userMenu').get('.MenuHeader').should('have.css', 'cursor', 'pointer').click();
|
||||
cy.uiOpenUserMenu().within(() => {
|
||||
// * Verify menu header is clickable
|
||||
cy.get('li').first().should('have.css', 'cursor', 'pointer').click();
|
||||
});
|
||||
|
||||
// * Verify click on header opens Profile settings modal
|
||||
cy.findByRole('dialog', {name: 'Profile'}).should('be.visible');
|
||||
@@ -111,14 +101,14 @@ function stepThroughStatuses(statusTestCases = []) {
|
||||
statusTestCases.forEach((tc) => {
|
||||
// # Open user menu and click option
|
||||
if (tc.text === 'Do not disturb') {
|
||||
cy.uiOpenDndStatusSubMenu().find('#dndTime-thirty_minutes_menuitem').click();
|
||||
cy.uiOpenDndStatusSubMenuAndClick30Mins();
|
||||
} else {
|
||||
cy.uiOpenUserMenu(tc.text);
|
||||
}
|
||||
|
||||
// # Verify correct status icon is shown on user's profile picture
|
||||
cy.uiGetProfileHeader().
|
||||
find('i').
|
||||
and('have.class', tc.profileClassName || tc.className);
|
||||
cy.uiGetSetStatusButton().within(() => {
|
||||
cy.get('svg').filter(`.${tc.className}`).should('have.length', 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('Notifications', () => {
|
||||
cy.uiOpenUserMenu().findByText(`@${firstUser.username}`);
|
||||
|
||||
// # Close the user menu
|
||||
cy.uiGetSetStatusButton().click();
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
// * Check that 'Town Square' is currently being selected
|
||||
cy.get('.active').within(() => {
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('Profile Settings', () => {
|
||||
fileTypes.forEach((fileType, index) => {
|
||||
it(`MM-T2078_${index + 1} Profile picture: file ${fileType.extension} type accepted`, () => {
|
||||
// # Save the default profile picture link so it can be compared to the new one
|
||||
cy.uiGetProfileHeader().findByRole('img').invoke('attr', 'src').as('defaultProfilePictureLink');
|
||||
cy.uiGetSetStatusButton().get('img').invoke('attr', 'src').as('defaultProfilePictureLink');
|
||||
|
||||
cy.uiOpenProfileModal('Profile Settings');
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('Profile Settings', () => {
|
||||
cy.uiClose();
|
||||
|
||||
// # Save the new custom profile picture link so it can be compared to the old one
|
||||
cy.uiGetProfileHeader().findByRole('img').invoke('attr', 'src').as('customProfilePictureLink');
|
||||
cy.uiGetSetStatusButton().get('img').invoke('attr', 'src').as('customProfilePictureLink');
|
||||
|
||||
cy.then(function() {
|
||||
expect(this.customProfilePictureLink).to.not.equal(this.defaultProfilePictureLink);
|
||||
|
||||
@@ -92,18 +92,18 @@ describe('Custom Theme - Sidebar Styles', () => {
|
||||
cy.uiOpenUserMenu('Online');
|
||||
|
||||
// * Check Online Indicator color
|
||||
cy.get('.icon-check-circle').should('have.css', 'color', rgbArrayToString(themeRgbColor.onlineIndicator));
|
||||
cy.uiGetSetStatusButton().find('svg').should('have.css', 'color', rgbArrayToString(themeRgbColor.onlineIndicator));
|
||||
|
||||
// # Set user status to away
|
||||
cy.uiOpenUserMenu('Away');
|
||||
|
||||
// * Check Away Indicator color
|
||||
cy.get('.icon-clock').should('have.css', 'color', rgbArrayToString(themeRgbColor.awayIndicator));
|
||||
cy.uiGetSetStatusButton().find('svg').should('have.css', 'color', rgbArrayToString(themeRgbColor.awayIndicator));
|
||||
|
||||
// # Set user status to do not disturb
|
||||
cy.uiOpenDndStatusSubMenu().find('#dndTime-thirty_minutes_menuitem').click();
|
||||
cy.uiOpenDndStatusSubMenuAndClick30Mins();
|
||||
|
||||
// * Check Do Not Disturb Indicator color
|
||||
cy.get('.icon-minus-circle').should('have.css', 'color', rgbArrayToString(themeRgbColor.dndIndicator));
|
||||
cy.uiGetSetStatusButton().find('svg').should('have.css', 'color', rgbArrayToString(themeRgbColor.dndIndicator));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('SignIn Authentication', () => {
|
||||
cy.url().should('include', '/channels/town-square');
|
||||
|
||||
// # Click logout via user menu
|
||||
cy.uiOpenUserMenu('Log Out');
|
||||
cy.uiOpenUserMenu('Log out');
|
||||
|
||||
// * Check that it logout successfully and it redirects into the login page
|
||||
cy.url().should('include', '/login');
|
||||
|
||||
@@ -186,7 +186,7 @@ describe('Login page with open server', () => {
|
||||
cy.url().should('include', '/channels/town-square');
|
||||
|
||||
// # Click logout via user menu
|
||||
cy.uiOpenUserMenu('Log Out');
|
||||
cy.uiOpenUserMenu('Log out');
|
||||
|
||||
// * Check that it logout successfully and it redirects into the login page
|
||||
cy.url().should('include', '/login');
|
||||
@@ -209,7 +209,7 @@ describe('Login page with open server', () => {
|
||||
cy.url().should('include', '/channels/town-square');
|
||||
|
||||
// # Click logout via user menu
|
||||
cy.uiOpenUserMenu('Log Out');
|
||||
cy.uiOpenUserMenu('Log out');
|
||||
|
||||
// * Check that it logout successfully and it redirects into the login page
|
||||
cy.url().should('include', '/login');
|
||||
|
||||
@@ -92,12 +92,12 @@ describe('Status of current user', () => {
|
||||
});
|
||||
|
||||
function verifyStatus(status: 'online' | 'away' | 'offline' | 'dnd') {
|
||||
cy.get('[aria-label="Current status: Online. Select to open profile and status menu."]').
|
||||
cy.get('[aria-label="Status is \\"Online\\". Open user\'s account menu."]').
|
||||
should(status === 'online' ? 'exist' : 'not.exist');
|
||||
cy.get('[aria-label="Current status: Away. Select to open profile and status menu."]').
|
||||
cy.get('[aria-label="Status is \\"Away\\". Open user\'s account menu."]').
|
||||
should(status === 'away' ? 'exist' : 'not.exist');
|
||||
cy.get('[aria-label="Current status: Offline. Select to open profile and status menu."]').
|
||||
cy.get('[aria-label="Status is \\"Offline\\". Open user\'s account menu."]').
|
||||
should(status === 'offline' ? 'exist' : 'not.exist');
|
||||
cy.get('[aria-label="Current status: Do not disturb. Select to open profile and status menu."]').
|
||||
cy.get('[aria-label="Status is \\"Do not disturb\\". Open user\'s account menu."]').
|
||||
should(status === 'dnd' ? 'exist' : 'not.exist');
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ Cypress.Commands.add('checkLeftSideBar', (settings = {}) => {
|
||||
cy.uiOpenUserMenu().findByText(`@${settings.user.username}`);
|
||||
|
||||
// # Close status menu
|
||||
cy.uiGetSetStatusButton().click();
|
||||
cy.get('body').type('{esc}');
|
||||
}
|
||||
|
||||
if (settings.user.userType === 'Admin' || settings.user.isAdmin) {
|
||||
|
||||
@@ -27,7 +27,8 @@ Cypress.Commands.add('uiOpenProductMenu', (item = '') => {
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetSetStatusButton', () => {
|
||||
return cy.findByRole('button', {name: /Select to open profile and status menu\./i}).should('be.visible');
|
||||
// # Get set status button
|
||||
return cy.get('#userAccountMenuButton').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetProfileHeader', () => {
|
||||
@@ -36,22 +37,18 @@ Cypress.Commands.add('uiGetProfileHeader', () => {
|
||||
|
||||
Cypress.Commands.add('uiGetStatusMenuContainer', (options = {exist: true}) => {
|
||||
if (options.exist) {
|
||||
return cy.findByRole('menu').should('exist');
|
||||
return cy.get('#userAccountMenu').should('exist').and('be.visible');
|
||||
}
|
||||
|
||||
return cy.findByRole('menu').should('not.exist');
|
||||
return cy.get('#userAccountMenu').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetStatusMenu', (options = {visible: true}) => {
|
||||
if (options.visible) {
|
||||
return cy.uiGetStatusMenuContainer().
|
||||
find('ul').
|
||||
should('be.visible');
|
||||
return cy.get('#userAccountMenu').should('exist').and('be.visible');
|
||||
}
|
||||
|
||||
return cy.uiGetStatusMenuContainer().
|
||||
find('ul').
|
||||
should('not.be.visible');
|
||||
return cy.get('#userAccountMenu').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiOpenHelpMenu', (item = '') => {
|
||||
@@ -147,7 +144,7 @@ Cypress.Commands.add('uiOpenSettingsModal', (section = '') => {
|
||||
|
||||
Cypress.Commands.add('uiLogout', () => {
|
||||
// # Click logout via user menu
|
||||
cy.uiOpenUserMenu('Log Out');
|
||||
cy.uiOpenUserMenu('Log out');
|
||||
|
||||
cy.url().should('include', '/login');
|
||||
cy.get('.login-body-message').should('be.visible');
|
||||
|
||||
@@ -33,14 +33,8 @@ Cypress.Commands.add('uiGetSystemConsoleMainMenu', () => {
|
||||
return getMenu(SYSTEM_CONSOLE_MAIN_MENU);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiOpenDndStatusSubMenu', () => {
|
||||
cy.uiOpenUserMenu();
|
||||
Cypress.Commands.add('uiOpenDndStatusSubMenuAndClick30Mins', () => {
|
||||
cy.uiOpenUserMenu().findByText('Do not disturb').should('be.visible').trigger('mouseover');
|
||||
|
||||
// # Wait for status menu to transition in
|
||||
cy.get('.MenuWrapper.status-dropdown-menu .Menu__content.dropdown-menu').should('be.visible');
|
||||
|
||||
// # Hover over Do Not Disturb option
|
||||
cy.get('.MenuWrapper.status-dropdown-menu .Menu__content.dropdown-menu li#status-menu-dnd_menuitem').trigger('mouseover');
|
||||
|
||||
return cy.get('#status-menu-dnd');
|
||||
cy.findByText('30 mins').should('be.visible').click({force: true});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
import React, {memo, useMemo, useRef} from 'react';
|
||||
import type {MouseEvent, KeyboardEvent} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {CustomStatusDuration} from '@mattermost/types/users';
|
||||
@@ -23,7 +24,7 @@ interface Props {
|
||||
spanStyle?: React.CSSProperties;
|
||||
emojiStyle?: React.CSSProperties;
|
||||
userID?: string;
|
||||
onClick?: () => void;
|
||||
onClick?: (event: MouseEvent<HTMLSpanElement> | KeyboardEvent<HTMLSpanElement>) => void;
|
||||
}
|
||||
|
||||
function CustomStatusEmoji({
|
||||
|
||||
@@ -57,7 +57,8 @@ const ExpiryTime = ({time, timezone, className, showPrefix, withinBrackets}: Pro
|
||||
id='custom_status.expiry.until'
|
||||
defaultMessage='Until {time}'
|
||||
values={{time: ''}}
|
||||
/>{' '}
|
||||
/>
|
||||
{' '}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import type {MouseEvent, KeyboardEvent} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getEmojiImageUrl} from 'mattermost-redux/utils/emoji_utils';
|
||||
@@ -14,7 +15,7 @@ interface ComponentProps {
|
||||
emojiName: string;
|
||||
size?: number;
|
||||
emojiStyle?: React.CSSProperties;
|
||||
onClick?: () => void;
|
||||
onClick?: (event: MouseEvent<HTMLSpanElement> | KeyboardEvent<HTMLSpanElement>) => void;
|
||||
}
|
||||
|
||||
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 StatusDropdown from 'components/status_dropdown';
|
||||
import {OnboardingTourSteps, OnboardingTourStepsForGuestUsers} from 'components/tours';
|
||||
import {
|
||||
CustomizeYourExperienceTour,
|
||||
useShowOnboardingTutorialStep,
|
||||
} from 'components/tours/onboarding_tour';
|
||||
import UserAccountMenu from 'components/user_account_menu';
|
||||
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
import {isChannels} from 'utils/products';
|
||||
@@ -46,10 +46,6 @@ const StyledCustomizeYourExperienceTour = styled.div`
|
||||
height: 100%
|
||||
`;
|
||||
|
||||
const StyledStatusDropdown = styled.div`
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
export type Props = {
|
||||
productId?: ProductIdentifier;
|
||||
}
|
||||
@@ -87,9 +83,7 @@ const RightControls = ({productId = null}: Props): JSX.Element => {
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
<StyledStatusDropdown>
|
||||
<StatusDropdown/>
|
||||
</StyledStatusDropdown>
|
||||
<UserAccountMenu/>
|
||||
</StyledCustomizeYourExperienceTour>
|
||||
</RightControlsContainer>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import './menu.scss';
|
||||
|
||||
export {Menu as Container} from './menu';
|
||||
export {SubMenu} from './sub_menu';
|
||||
export {MenuItem as Item} from './menu_item';
|
||||
export type {FirstMenuItemProps} from './menu_item';
|
||||
export {MenuItemSeparator as Separator} from './menu_item_separator';
|
||||
|
||||
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.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import MuiMenu from '@mui/material/Menu';
|
||||
import MuiMenuList from '@mui/material/MenuList';
|
||||
import type {PopoverOrigin} from '@mui/material/Popover';
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
@@ -28,7 +31,8 @@ import Constants, {A11yClassNames} from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
|
||||
import {MenuContext, useMenuContextValue} from './menu_context';
|
||||
import {MuiMenuStyled} from './menu_styled';
|
||||
|
||||
import './menu.scss';
|
||||
|
||||
const MENU_OPEN_ANIMATION_DURATION = 150;
|
||||
const MENU_CLOSE_ANIMATION_DURATION = 100;
|
||||
@@ -37,6 +41,7 @@ type MenuButtonProps = {
|
||||
id: string;
|
||||
dateTestId?: string;
|
||||
'aria-label'?: string;
|
||||
'aria-describedby'?: string;
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
as?: keyof JSX.IntrinsicElements;
|
||||
@@ -55,18 +60,15 @@ type MenuProps = {
|
||||
'aria-label'?: string;
|
||||
|
||||
/**
|
||||
* @warning Make the styling of your components such a way that they dont need this handler
|
||||
* @warning Make the styling of your components such a way that they don't need this handler
|
||||
*/
|
||||
onToggle?: (isOpen: boolean) => void;
|
||||
onKeyDown?: (event: KeyboardEvent<HTMLDivElement>, forceCloseMenu?: () => void) => void;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const defaultAnchorOrigin = {vertical: 'bottom', horizontal: 'left'};
|
||||
const defaultTransformOrigin = {vertical: 'top', horizontal: 'left'};
|
||||
|
||||
type VerticalOrigin = 'top' | 'center' | 'bottom';
|
||||
type HorizontalOrigin = 'left' | 'center' | 'right';
|
||||
const defaultAnchorOrigin = {vertical: 'bottom', horizontal: 'left'} as PopoverOrigin;
|
||||
const defaultTransformOrigin = {vertical: 'top', horizontal: 'left'} as PopoverOrigin;
|
||||
|
||||
interface Props {
|
||||
menuButton: MenuButtonProps;
|
||||
@@ -76,14 +78,8 @@ interface Props {
|
||||
|
||||
// Use MUI Anchor Playgroup to try various anchorOrigin
|
||||
// and transformOrigin values - https://mui.com/material-ui/react-popover/#anchor-playground
|
||||
anchorOrigin?: {
|
||||
vertical: VerticalOrigin;
|
||||
horizontal: HorizontalOrigin;
|
||||
};
|
||||
transformOrigin?: {
|
||||
vertical: VerticalOrigin;
|
||||
horizontal: HorizontalOrigin;
|
||||
};
|
||||
anchorOrigin?: PopoverOrigin;
|
||||
transformOrigin?: PopoverOrigin;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,7 +103,7 @@ export function Menu(props: Props) {
|
||||
const [disableAutoFocusItem, setDisableAutoFocusItem] = useState(false);
|
||||
const isMenuOpen = Boolean(anchorElement);
|
||||
|
||||
// Callback funtion handler called when menu is closed by escapeKeyDown, backdropClick or tabKeyDown
|
||||
// Callback function handler called when menu is closed by escapeKeyDown, backdropClick or tabKeyDown
|
||||
function handleMenuClose(event: MouseEvent<HTMLDivElement>) {
|
||||
event.preventDefault();
|
||||
setAnchorElement(null);
|
||||
@@ -125,9 +121,9 @@ export function Menu(props: Props) {
|
||||
setAnchorElement(null);
|
||||
}
|
||||
|
||||
// Stop sythetic events from bubbling up to the parent
|
||||
// Stop synthetic events from bubbling up to the parent
|
||||
// @see https://github.com/mui/material-ui/issues/32064
|
||||
function handleMenuClick(e: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) {
|
||||
function handleMenuClick(e: MouseEvent<HTMLDivElement> | KeyboardEvent<HTMLDivElement>) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
@@ -193,7 +189,8 @@ export function Menu(props: Props) {
|
||||
aria-haspopup={true}
|
||||
aria-expanded={isMenuOpen}
|
||||
disabled={props.menuButton?.disabled ?? false}
|
||||
aria-label={props.menuButton?.['aria-label'] ?? ''}
|
||||
aria-label={props.menuButton?.['aria-label']}
|
||||
aria-describedby={props.menuButton?.['aria-describedby']}
|
||||
className={props.menuButton?.class ?? ''}
|
||||
onClick={handleMenuButtonClick}
|
||||
onMouseDown={handleMenuButtonMouseDown}
|
||||
@@ -234,21 +231,23 @@ export function Menu(props: Props) {
|
||||
<CompassDesignProvider theme={theme}>
|
||||
{renderMenuButton()}
|
||||
<MenuContext.Provider value={providerValue}>
|
||||
<MuiMenuStyled
|
||||
<MuiMenu
|
||||
anchorEl={anchorElement}
|
||||
open={isMenuOpen}
|
||||
onClose={handleMenuClose}
|
||||
onClick={handleMenuClick}
|
||||
onTransitionExited={providerValue.handleClosed}
|
||||
onKeyDown={handleMenuKeyDown}
|
||||
className={A11yClassNames.POPUP}
|
||||
width={props.menu.width}
|
||||
className={classNames(A11yClassNames.POPUP, 'menu_menuStyled')}
|
||||
marginThreshold={0}
|
||||
anchorOrigin={props.anchorOrigin || defaultAnchorOrigin}
|
||||
transformOrigin={props.transformOrigin || defaultTransformOrigin}
|
||||
disableAutoFocusItem={disableAutoFocusItem} // This is not anti-pattern, see handleMenuButtonMouseDown
|
||||
MenuListProps={{
|
||||
id: props.menu.id,
|
||||
'aria-label': props.menu?.['aria-label'] ?? '',
|
||||
style: {
|
||||
width: props.menu.width || 'inherit',
|
||||
},
|
||||
}}
|
||||
TransitionProps={{
|
||||
mountOnEnter: true,
|
||||
@@ -258,9 +257,12 @@ export function Menu(props: Props) {
|
||||
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}
|
||||
</MuiMenuStyled>
|
||||
</MuiMenu>
|
||||
</MenuContext.Provider>
|
||||
</CompassDesignProvider>
|
||||
);
|
||||
|
||||
@@ -89,6 +89,25 @@ export interface Props extends MuiMenuItemProps {
|
||||
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.
|
||||
* Checkout Compass's Menu Item(compass.mattermost.com) for terminology, styling and usage guidelines.
|
||||
@@ -161,7 +180,11 @@ export function MenuItem(props: Props) {
|
||||
}
|
||||
|
||||
// When both primary and secondary labels are passed, we need to apply minor changes to the styling. Check below in styled component for more details.
|
||||
const hasSecondaryLabel = labels && labels.props && labels.props.children && Children.count(labels.props.children) === 2;
|
||||
// we count after converting to array as it removes falsy values from labels.props.children
|
||||
const hasSecondaryLabel = labels &&
|
||||
labels.props &&
|
||||
labels.props.children &&
|
||||
Children.count(Children.toArray(labels.props.children)) === 2;
|
||||
|
||||
return (
|
||||
<MenuItemStyled
|
||||
@@ -283,7 +306,6 @@ export const MenuItemStyled = styled(MuiMenuItem, {
|
||||
justifyContent: 'flex-end',
|
||||
color: isRegular ? 'rgba(var(--center-channel-color-rgb), 0.75)' : 'var(--error-text)',
|
||||
gap: '4px',
|
||||
marginInlineStart: '24px',
|
||||
fontSize: '12px',
|
||||
lineHeight: '16px',
|
||||
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.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import MuiMenu from '@mui/material/Menu';
|
||||
import MuiMenuList from '@mui/material/MenuList';
|
||||
import type {PopoverOrigin} from '@mui/material/Popover';
|
||||
import React, {
|
||||
@@ -31,9 +32,6 @@ import {isKeyPressed} from 'utils/keyboard';
|
||||
import {SubMenuContext, useMenuContextValue} from './menu_context';
|
||||
import {MenuItem} from './menu_item';
|
||||
import type {Props as MenuItemProps} from './menu_item';
|
||||
import {MuiMenuStyled} from './menu_styled';
|
||||
|
||||
import './sub_menu.scss';
|
||||
|
||||
interface Props {
|
||||
id: MenuItemProps['id'];
|
||||
@@ -41,10 +39,12 @@ interface Props {
|
||||
labels: MenuItemProps['labels'];
|
||||
trailingElements?: MenuItemProps['trailingElements'];
|
||||
isDestructive?: MenuItemProps['isDestructive'];
|
||||
role?: MenuItemProps['role'];
|
||||
|
||||
// Menu props
|
||||
menuId: string;
|
||||
menuAriaLabel?: string;
|
||||
menuAriaDescribedBy?: string;
|
||||
forceOpenOnLeft?: boolean; // Most of the times this is not needed, since submenu position is calculated and placed
|
||||
|
||||
children: ReactNode;
|
||||
@@ -57,8 +57,10 @@ export function SubMenu(props: Props) {
|
||||
labels,
|
||||
trailingElements,
|
||||
isDestructive,
|
||||
role,
|
||||
menuId,
|
||||
menuAriaLabel,
|
||||
menuAriaDescribedBy,
|
||||
forceOpenOnLeft,
|
||||
children,
|
||||
...rest
|
||||
@@ -148,6 +150,7 @@ export function SubMenu(props: Props) {
|
||||
labels,
|
||||
trailingElements,
|
||||
isDestructive,
|
||||
role,
|
||||
onClick: isMobileView ? handleOnClick : undefined, // OnClicks on parent menuItem of subMenu is only needed in mobile view
|
||||
};
|
||||
|
||||
@@ -163,13 +166,13 @@ export function SubMenu(props: Props) {
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<MuiMenuStyled
|
||||
<MuiMenu
|
||||
anchorEl={anchorElement}
|
||||
open={isSubMenuOpen}
|
||||
asSubMenu={true}
|
||||
anchorOrigin={originOfAnchorAndTransform.anchorOrigin}
|
||||
transformOrigin={originOfAnchorAndTransform.transformOrigin}
|
||||
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 */}
|
||||
{/* pointer turns to default as soon as it leaves the parent menu */}
|
||||
@@ -178,6 +181,7 @@ export function SubMenu(props: Props) {
|
||||
id={menuId}
|
||||
component='ul'
|
||||
aria-label={menuAriaLabel}
|
||||
aria-describedby={menuAriaDescribedBy}
|
||||
className={A11yClassNames.POPUP}
|
||||
onKeyDown={handleSubMenuKeyDown}
|
||||
sx={{
|
||||
@@ -190,7 +194,7 @@ export function SubMenu(props: Props) {
|
||||
{children}
|
||||
</SubMenuContext.Provider>
|
||||
</MuiMenuList>
|
||||
</MuiMenuStyled>
|
||||
</MuiMenu>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const CompleteYourProfileTour = () => {
|
||||
</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) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -64,6 +64,7 @@ export const CompleteYourProfileTour = () => {
|
||||
singleTip={true}
|
||||
showOptOut={false}
|
||||
interactivePunchOut={true}
|
||||
zIndex={1058}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
switchToChannels,
|
||||
} from 'actions/views/onboarding_tasks';
|
||||
import {setProductMenuSwitcherOpen} from 'actions/views/product_menu';
|
||||
import {setStatusDropdown} from 'actions/views/status_dropdown';
|
||||
import {getOnboardingTaskPreferences} from 'selectors/onboarding';
|
||||
|
||||
import Channels from 'components/common/svg_images_components/channels_svg';
|
||||
@@ -40,6 +39,7 @@ import {
|
||||
} from 'components/tours';
|
||||
|
||||
import {ModalIdentifiers, TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
import {clickOnMenuButton, MenuButtonIds} from 'utils/keyboard';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
@@ -239,7 +239,7 @@ export const useHandleOnBoardingTaskTrigger = () => {
|
||||
break;
|
||||
}
|
||||
case OnboardingTasksName.COMPLETE_YOUR_PROFILE: {
|
||||
dispatch(setStatusDropdown(true));
|
||||
clickOnMenuButton(MenuButtonIds.userAccountMenu);
|
||||
dispatch(setShowOnboardingCompleteProfileTour(true));
|
||||
handleSaveData(taskName, TaskNameMapToSteps[taskName].STARTED, true);
|
||||
if (inAdminConsole) {
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {setStatusDropdown} from 'actions/views/status_dropdown';
|
||||
import {makeGetCustomStatus, showPostHeaderUpdateStatusButton, isCustomStatusEnabled} from 'selectors/views/custom_status';
|
||||
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import EmojiIcon from 'components/widgets/icons/emoji_icon';
|
||||
|
||||
import {clickOnMenuButton, MenuButtonIds} from 'utils/keyboard';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
interface ComponentProps {
|
||||
@@ -22,7 +23,6 @@ interface ComponentProps {
|
||||
const PostHeaderCustomStatus = (props: ComponentProps) => {
|
||||
const getCustomStatus = useMemo(makeGetCustomStatus, []);
|
||||
const {userId, isSystemMessage, isBot} = props;
|
||||
const dispatch = useDispatch();
|
||||
const userCustomStatus = useSelector((state: GlobalState) => getCustomStatus(state, userId));
|
||||
const showUpdateStatusButton = useSelector(showPostHeaderUpdateStatusButton);
|
||||
const customStatusEnabled = useSelector(isCustomStatusEnabled);
|
||||
@@ -51,7 +51,7 @@ const PostHeaderCustomStatus = (props: ComponentProps) => {
|
||||
|
||||
const updateStatus = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
dispatch(setStatusDropdown(true));
|
||||
clickOnMenuButton(MenuButtonIds.userAccountMenu);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -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'
|
||||
>
|
||||
<span className='status-wrapper'>
|
||||
|
||||
<Avatar
|
||||
username={user.username}
|
||||
size={'sm'}
|
||||
|
||||
@@ -3556,7 +3556,6 @@
|
||||
"custom_status.expiry_dropdown.today": "Today",
|
||||
"custom_status.expiry.time_picker.title": "Time",
|
||||
"custom_status.expiry.until": "Until {time}",
|
||||
"custom_status.expiry.until_tomorrow": "Until Tomorrow {time}",
|
||||
"custom_status.modal_cancel": "Clear Status",
|
||||
"custom_status.modal_confirm": "Set Status",
|
||||
"custom_status.set_status": "Set a status",
|
||||
@@ -5259,29 +5258,6 @@
|
||||
"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.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.subtitle": "Your final bill will be prorated. Your workspace now has {plan} limits.",
|
||||
"suggestion.all": "All channel types",
|
||||
@@ -5935,6 +5911,39 @@
|
||||
"user.settings.tokens.tokenId": "Token ID: ",
|
||||
"user.settings.tokens.tokenLoading": "Loading...",
|
||||
"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.helpResources": "Help resources",
|
||||
"userGuideHelp.keyboardShortcuts": "Keyboard shortcuts",
|
||||
|
||||
@@ -25,7 +25,6 @@ import rhs from './rhs';
|
||||
import rhsSuppressed from './rhs_suppressed';
|
||||
import search from './search';
|
||||
import settings from './settings';
|
||||
import statusDropdown from './status_dropdown';
|
||||
import system from './system';
|
||||
import textbox from './textbox';
|
||||
import threads from './threads';
|
||||
@@ -50,7 +49,6 @@ export default combineReducers({
|
||||
marketplace,
|
||||
textbox,
|
||||
channelSidebar,
|
||||
statusDropdown,
|
||||
addChannelDropdown,
|
||||
addChannelCtaDropdown,
|
||||
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.
|
||||
$z-index-popover: 1070;
|
||||
$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-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;
|
||||
};
|
||||
|
||||
statusDropdown: {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
addChannelDropdown: {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
@@ -250,7 +250,6 @@ export const ActionTypes = keyMirror({
|
||||
INCREMENT_EMOJI_PICKER_PAGE: null,
|
||||
SET_RECENT_SKIN: null,
|
||||
|
||||
STATUS_DROPDOWN_TOGGLE: null,
|
||||
ADD_CHANNEL_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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@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