From 3902d00d0f08b2362a8716b311867ae2585b7243 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 25 Feb 2025 16:20:00 -0500 Subject: [PATCH 01/63] MM-61947 Run DND expiry job more often and round expiry time to match interval (#29938) * MM-61947 Run DND expiry job more often and round expiry time to match interval * Move comment to make it godoc-compatible * Change truncateDNDEndTime to work with seconds --------- Co-authored-by: Mattermost Build --- server/channels/app/platform/status.go | 16 +++++++++++++++- server/channels/app/platform/status_test.go | 15 +++++++++++++++ server/channels/app/server.go | 4 ++-- server/public/model/status.go | 12 ++++++++++-- .../dnd_custom_time_picker_modal.tsx | 6 +++--- .../dot_menu/post_reminder_submenu.tsx | 4 ++-- .../post_reminder_custom_time_picker_modal.tsx | 4 ++-- webapp/channels/src/utils/datetime.ts | 2 +- webapp/platform/types/src/users.ts | 5 +++++ 9 files changed, 55 insertions(+), 13 deletions(-) diff --git a/server/channels/app/platform/status.go b/server/channels/app/platform/status.go index 5aee2f3abba..d8bf6609e39 100644 --- a/server/channels/app/platform/status.go +++ b/server/channels/app/platform/status.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "net/http" + "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" @@ -417,7 +418,7 @@ func (ps *PlatformService) SetStatusDoNotDisturbTimed(userID string, endtime int status.Status = model.StatusDnd status.Manual = true - status.DNDEndTime = endtime + status.DNDEndTime = truncateDNDEndTime(endtime) ps.SaveAndBroadcastStatus(status) if ps.sharedChannelService != nil { @@ -425,6 +426,19 @@ func (ps *PlatformService) SetStatusDoNotDisturbTimed(userID string, endtime int } } +// truncateDNDEndTime takes a user-provided timestamp (in seconds) for when their DND expiry should end and truncates +// it to line up with the DND expiry job so that the user's DND time doesn't expire late by an interval. The job to +// expire statuses runs every minute currently, so this trims the seconds and milliseconds off the given timestamp. +// +// This will result in statuses expiring slightly earlier than specified in the UI, but the status will expire at +// the correct time on the wall clock. For example, if the time is currently 13:04:29 and the user sets the expiry to +// 5 minutes, truncating will make the status will expire at 13:09:00 instead of at 13:10:00. +// +// Note that the timestamps used by this are in seconds, not milliseconds. This matches UserStatus.DNDEndTime. +func truncateDNDEndTime(endtime int64) int64 { + return time.Unix(endtime, 0).Truncate(model.DNDExpiryInterval).Unix() +} + func (ps *PlatformService) SetStatusDoNotDisturb(userID string) { if !*ps.Config().ServiceSettings.EnableUserStatuses { return diff --git a/server/channels/app/platform/status_test.go b/server/channels/app/platform/status_test.go index bbb4d6d57a8..7f791d555a5 100644 --- a/server/channels/app/platform/status_test.go +++ b/server/channels/app/platform/status_test.go @@ -6,6 +6,7 @@ package platform import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" @@ -37,3 +38,17 @@ func TestSaveStatus(t *testing.T) { }) } } + +func TestTruncateDNDEndTime(t *testing.T) { + // 2025-Jan-20 at 17:13:32 GMT becomes 17:13:00 + assert.Equal(t, int64(1737393180), truncateDNDEndTime(1737393212)) + + // 2025-Jan-20 at 17:13:00 GMT remains unchanged + assert.Equal(t, int64(1737393180), truncateDNDEndTime(1737393180)) + + // 2025-Jan-20 at 00:00:10 GMT becomes 00:00:00 + assert.Equal(t, int64(1737331200), truncateDNDEndTime(1737331210)) + + // 2025-Jan-20 at 00:00:10 GMT remains unchanged + assert.Equal(t, int64(1737331200), truncateDNDEndTime(1737331200)) +} diff --git a/server/channels/app/server.go b/server/channels/app/server.go index 5a8b815279e..65184880726 100644 --- a/server/channels/app/server.go +++ b/server/channels/app/server.go @@ -1782,14 +1782,14 @@ func cancelTask(mut *sync.Mutex, taskPointer **model.ScheduledTask) { func runDNDStatusExpireJob(a *App) { if a.IsLeader() { withMut(&a.ch.dndTaskMut, func() { - a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, 5*time.Minute) + a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, model.DNDExpiryInterval) }) } a.ch.srv.AddClusterLeaderChangedListener(func() { mlog.Info("Cluster leader changed. Determining if unset DNS status task should be running", mlog.Bool("isLeader", a.IsLeader())) if a.IsLeader() { withMut(&a.ch.dndTaskMut, func() { - a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, 5*time.Minute) + a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, model.DNDExpiryInterval) }) } else { cancelTask(&a.ch.dndTaskMut, &a.ch.dndTask) diff --git a/server/public/model/status.go b/server/public/model/status.go index a0c5d073f3d..6c400063fec 100644 --- a/server/public/model/status.go +++ b/server/public/model/status.go @@ -5,6 +5,7 @@ package model import ( "encoding/json" + "time" ) const ( @@ -16,6 +17,9 @@ const ( StatusCacheSize = SessionCacheSize StatusChannelTimeout = 20000 // 20 seconds StatusMinUpdateTime = 120000 // 2 minutes + + // DNDExpiryInterval is how often the job to expire temporary DND statuses runs. + DNDExpiryInterval = 1 * time.Minute ) type Status struct { @@ -24,8 +28,12 @@ type Status struct { Manual bool `json:"manual"` LastActivityAt int64 `json:"last_activity_at"` ActiveChannel string `json:"active_channel,omitempty" db:"-"` - DNDEndTime int64 `json:"dnd_end_time"` - PrevStatus string `json:"-"` + + // DNDEndTime is the time that the user's DND status will expire. Unlike other timestamps in Mattermost, this value + // is in seconds instead of milliseconds. + DNDEndTime int64 `json:"dnd_end_time"` + + PrevStatus string `json:"-"` } func (s *Status) ToJSON() ([]byte, error) { diff --git a/webapp/channels/src/components/dnd_custom_time_picker_modal/dnd_custom_time_picker_modal.tsx b/webapp/channels/src/components/dnd_custom_time_picker_modal/dnd_custom_time_picker_modal.tsx index d68d576c9e4..92f9e06db3f 100644 --- a/webapp/channels/src/components/dnd_custom_time_picker_modal/dnd_custom_time_picker_modal.tsx +++ b/webapp/channels/src/components/dnd_custom_time_picker_modal/dnd_custom_time_picker_modal.tsx @@ -21,7 +21,7 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper'; import Constants, {A11yCustomEventTypes, UserStatuses} from 'utils/constants'; import type {A11yFocusEventDetail} from 'utils/constants'; -import {toUTCUnix} from 'utils/datetime'; +import {toUTCUnixInSeconds} from 'utils/datetime'; import {isKeyPressed} from 'utils/keyboard'; import {localizeMessage} from 'utils/utils'; @@ -126,9 +126,9 @@ export default class DndCustomTimePicker extends React.PureComponent { - actions.addPostReminder(userId, postId, toUTCUnix(dateTime.toDate())); + actions.addPostReminder(userId, postId, toUTCUnixInSeconds(dateTime.toDate())); onExited(); }, [actions, postId, userId, onExited]); diff --git a/webapp/channels/src/utils/datetime.ts b/webapp/channels/src/utils/datetime.ts index a359f392fb8..7c957f5a515 100644 --- a/webapp/channels/src/utils/datetime.ts +++ b/webapp/channels/src/utils/datetime.ts @@ -86,7 +86,7 @@ export function isYesterday(date: Date): boolean { return isSameDay(date, yesterday); } -export function toUTCUnix(date: Date): number { +export function toUTCUnixInSeconds(date: Date): number { return Math.round(new Date(date.toISOString()).getTime() / 1000); } diff --git a/webapp/platform/types/src/users.ts b/webapp/platform/types/src/users.ts index 8ce683bdfb3..16bdf590e3b 100644 --- a/webapp/platform/types/src/users.ts +++ b/webapp/platform/types/src/users.ts @@ -100,6 +100,11 @@ export type UserStatus = { manual?: boolean; last_activity_at?: number; active_channel?: string; + + /** + * The time when a user's timed DND status will expire. Unlike other timestamps in the app, this is in seconds + * instead of milliseconds. + */ dnd_end_time?: number; }; From 6ecaad2a41a423a6beb697ffbd03fd6b808e42c1 Mon Sep 17 00:00:00 2001 From: ayush-chauhan233 Date: Wed, 26 Feb 2025 03:29:30 +0530 Subject: [PATCH 02/63] MM-61652: Removed heading related role from channel header (#30277) --- .../playbooks/channels/rhs/about_spec.js | 2 +- .../channel_header/channel_header_title.tsx | 61 ++++++++++--------- webapp/channels/src/sass/layout/_headers.scss | 1 - 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/about_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/about_spec.js index 64bf07efd76..7e6465ee173 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/about_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/rhs/about_spec.js @@ -90,7 +90,7 @@ describe('channels > rhs > header', {testIsolation: true}, () => { cy.get('#rhsContainer').findByTestId('rendered-run-name').should('be.visible').contains('new run name'); // * make sure the channel name remains unchanged - cy.get('#channelHeaderInfo').findByRole('heading').contains(playbookRunName); + cy.get('#channelHeaderInfo').contains(playbookRunName); }); }); diff --git a/webapp/channels/src/components/channel_header/channel_header_title.tsx b/webapp/channels/src/components/channel_header/channel_header_title.tsx index 9e2b303cb4c..1806c5cdad4 100644 --- a/webapp/channels/src/components/channel_header/channel_header_title.tsx +++ b/webapp/channels/src/components/channel_header/channel_header_title.tsx @@ -105,8 +105,6 @@ const ChannelHeaderTitle = ({ size='sm' /> @@ -148,46 +146,49 @@ const ChannelHeaderTitle = ({
- - - + + )} +
Date: Wed, 26 Feb 2025 03:43:51 +0530 Subject: [PATCH 03/63] [MM-61647][MM-61651]: Upgraded react-select to v5 (#30070) * [MM-61647]: Upgraded react-select to v5 - Updated old type definitions with new v5 packaged type definitions. - Removed some unused props - Updated onBlur, onFocus and handleInputChange methods in user_input_email component * Fix incorrect usage of handleInputChange in ChannelsInput * Remove type assertions from dropdown_input_hybrid.tsx * Simplify typing of users_emails_input.tsx * [MM-61647][MM-61651]: Fixed some type definitions and e2e failing test case * [MM-61647][MM-61651]: Fixed type error in dropdown_input_hybrid * [MM-61647][MM-61651]: Fixed failing CI type error and e2e test cases * [MM-61647][MM-61651]: Fixed failing e2e test case * [MM-61647][MM-61651]: Updated the styles and reverted the test case changes - Fixed the theme not getting correctly inherited - Fixed the timezone and language settings getting saved on enter --------- Co-authored-by: Harrison Healey Co-authored-by: Mattermost Build --- .../accessibility_account_settings_spec.js | 45 ++- .../convert_group_message_to_private_spec.ts | 2 +- .../guest_identification_ui_spec.ts | 6 +- .../system_console/channel_members_spec.js | 2 +- .../enterprise/system_console/helpers.js | 2 +- .../user_management_admin_control_spec.js | 2 +- .../messaging/dm_list_of_users_spec.js | 2 +- .../messaging/header_not_cloud_spec.js | 2 +- .../dm_more_searching_from_page_spec.js | 2 +- .../dm_more_show_user_count_spec.js | 2 +- .../multi_team_and_dm/gm_add_user_spec.js | 2 +- .../users_deactivation_spec.js | 4 +- .../closed_team_invite_by_email_spec.js | 2 +- .../invite_user_to_closed_team_spec.js | 2 +- e2e-tests/cypress/tests/support/ui/channel.js | 2 +- .../tests/support/ui/data_retention.js | 4 +- .../cypress/tests/support/ui_commands.ts | 2 +- webapp/channels/package.json | 3 +- .../schema_admin_settings.test.tsx.snap | 2 +- .../data_retention_settings.tsx | 3 +- .../global_policy_form/global_policy_form.tsx | 4 +- .../team_filter_dropdown.tsx | 14 +- .../admin_console/list_table/list_table.tsx | 6 +- .../admin_console/multiselect_settings.tsx | 12 +- .../admin_console/schema_admin_settings.tsx | 6 +- .../styled_users_filters_status/index.tsx | 1 - .../system_users_filter_role/index.tsx | 1 - .../system_users_filter_team/index.tsx | 38 +- .../apps_form_select_field.tsx | 28 +- .../apps_form_field/select_channel_option.tsx | 4 +- .../apps_form_field/select_user_option.tsx | 3 +- .../channel_notifications_modal.test.tsx.snap | 140 +++++-- .../channel_notifications_modal.tsx | 4 +- .../select_text_input/select_text_input.tsx | 15 +- .../src/components/dropdown_input.tsx | 23 +- .../forward_post_channel_select.tsx | 18 +- .../forward_post_channel_select_styles.tsx | 38 +- .../components/forward_post_modal/index.tsx | 4 +- .../move_thread_modal/move_thread_modal.tsx | 4 +- .../__snapshots__/multiselect.test.tsx.snap | 15 +- .../components/multiselect/multiselect.tsx | 17 +- .../multiselect/multiselect_list.tsx | 4 +- .../invite_members.test.tsx.snap | 93 +++-- .../start_trial_form_modal.test.tsx.snap | 122 +++--- .../start_trial_form_modal/index.tsx | 2 +- .../team_access_tab/team_access_tab.test.tsx | 2 +- .../manage_languages/manage_languages.tsx | 23 +- .../manage_timezones/manage_timezones.tsx | 11 +- .../__snapshots__/index.test.tsx.snap | 72 +++- .../index.tsx | 14 +- .../index.tsx | 8 +- .../user_settings_notifications.tsx | 10 +- .../limit_visible_gms_dms.tsx | 10 +- .../dropdown_input_hybrid.test.tsx.snap | 5 +- .../widgets/inputs/channels_input.test.tsx | 4 +- .../widgets/inputs/channels_input.tsx | 39 +- .../widgets/inputs/dropdown_input_hybrid.tsx | 44 ++- .../widgets/inputs/users_emails_input.scss | 4 + .../inputs/users_emails_input.test.tsx | 4 +- .../widgets/inputs/users_emails_input.tsx | 60 +-- .../components/checkbox_with_select_item.tsx | 5 +- .../modals/components/react_select_item.tsx | 5 +- .../src/sass/components/_react-select.scss | 4 + webapp/package-lock.json | 372 +++++++++--------- 64 files changed, 784 insertions(+), 621 deletions(-) diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js index ed166583590..50d67e3ace1 100644 --- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js @@ -169,36 +169,43 @@ describe('Verify Accessibility Support in different sections in Settings and Pro cy.get('#displayButton').click(); cy.get('#languagesEdit').click(); - cy.get('#displayLanguage').within(() => { - cy.get('input').should('have.attr', 'aria-autocomplete', 'list').and('have.attr', 'aria-labelledby', 'changeInterfaceLanguageLabel').as('inputEl'); - }); + cy.findByRole('combobox', {name: 'Dropdown selector to change the interface language'}).should('have.attr', 'aria-autocomplete', 'list').and('have.attr', 'aria-labelledby', 'changeInterfaceLanguageLabel').as('inputEl'); cy.get('#changeInterfaceLanguageLabel').should('be.visible').and('have.text', 'Change interface language'); - // # When enter key is pressed on dropdown, it should expand and collapse - cy.get('@inputEl').typeWithForce('{enter}'); - cy.get('#displayLanguage>div').should('have.class', 'react-select__control--menu-is-open'); - cy.get('@inputEl').typeWithForce('{enter}'); - cy.get('#displayLanguage>div').should('not.have.class', 'react-select__control--menu-is-open'); + // # When space key is pressed on dropdown, it should expand and should collapse when esc key is pressed + cy.get('@inputEl').typeWithForce(' '); + cy.findByRole('listbox').should('have.class', 'react-select__menu-list').as('listBox'); + cy.get('@inputEl').typeWithForce('{esc}'); + cy.get('@listBox').should('not.exist'); // # Press down arrow twice and check aria label - cy.get('@inputEl').typeWithForce('{enter}'); + cy.get('@inputEl').typeWithForce(' '); cy.get('@inputEl').typeWithForce('{downarrow}{downarrow}'); - cy.get('#displayLanguage>span').as('ariaEl').within(($el) => { - cy.wrap($el).should('have.attr', 'aria-live', 'assertive'); - cy.get('#aria-context').should('contain', 'option English (Australia) focused').and('contain', 'Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.'); + cy.get('#displayLanguage').within(($el) => { + cy.wrap($el).findByRole('log').should('have.attr', 'aria-live', 'assertive').as('ariaEl'); + }); + cy.get('@ariaEl').within(($el) => { + cy.wrap($el).get('#aria-focused').should('contain', 'option English (Australia) focused'); + cy.wrap($el).get('#aria-guidance').should('contain', 'Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.'); }); - // # Check if language setting gets changed after user presses enter - cy.get('@inputEl').typeWithForce('{enter}'); + // # Check if language setting gets changed after user presses space + cy.get('@inputEl').typeWithForce(' '); cy.get('#displayLanguage').should('contain', 'English (Australia)'); - cy.get('@ariaEl').get('#aria-selection-event').should('contain', 'option English (Australia), selected'); + cy.get('@ariaEl').within(($el) => { + cy.wrap($el).get('#aria-selection').should('contain', 'option English (Australia) selected'); + }); - // # Press down arrow, then up arrow and press enter + // # Press down arrow, then up arrow and press space cy.get('@inputEl').typeWithForce('{downarrow}{downarrow}{downarrow}{uparrow}'); - cy.get('@ariaEl').get('#aria-context').should('contain', 'option English (US) focused'); - cy.get('@inputEl').typeWithForce('{enter}'); + cy.get('@ariaEl').within(($el) => { + cy.wrap($el).get('#aria-focused').should('contain', 'option English (US) focused'); + }); + cy.get('@inputEl').typeWithForce(' '); cy.get('#displayLanguage').should('contain', 'English (US)'); - cy.get('@ariaEl').get('#aria-selection-event').should('contain', 'option English (US), selected'); + cy.get('@ariaEl').within(($el) => { + cy.wrap($el).get('#aria-selection').should('contain', 'option English (US) selected'); + }); }); it('MM-T1488 Profile Picture should read labels', () => { diff --git a/e2e-tests/cypress/tests/integration/channels/channel/convert_group_message_to_private_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel/convert_group_message_to_private_spec.ts index 4f49e2ec71e..08f7650a072 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/convert_group_message_to_private_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel/convert_group_message_to_private_spec.ts @@ -67,7 +67,7 @@ describe('Group Message Conversion To Private Channel', () => { cy.get('.GenericModal__button.delete.disabled').wait(2000); // Open the team dropdown and select a team - cy.findByText('Select Team').click(); + cy.findByText('Select Team').click({force: true}); cy.findByText(testTeam2.display_name).click(); // Enter the new channel name and confirm diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts index 592143e0783..62712fd3fa2 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_ui_spec.ts @@ -166,7 +166,7 @@ describe('Verify Guest User Identification in different screens', () => { // # Open a DM with Guest User cy.uiAddDirectMessage().click(); cy.findByRole('dialog', {name: 'Direct Messages'}).should('be.visible').wait(TIMEOUTS.ONE_SEC); - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('combobox', {name: 'Search for people'}). should('have.focused'). typeWithForce(guestUser.username). wait(TIMEOUTS.ONE_SEC). @@ -182,12 +182,12 @@ describe('Verify Guest User Identification in different screens', () => { // # Open a GM with Guest User and Sysadmin cy.uiAddDirectMessage().click(); cy.findByRole('dialog', {name: 'Direct Messages'}).should('be.visible').wait(TIMEOUTS.ONE_SEC); - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('combobox', {name: 'Search for people'}). should('have.focused'). typeWithForce(guestUser.username). wait(TIMEOUTS.ONE_SEC). typeWithForce('{enter}'); - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('combobox', {name: 'Search for people'}). should('have.focused'). typeWithForce(admin.username). wait(TIMEOUTS.ONE_SEC). diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/channel_members_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/channel_members_spec.js index 964cfc0845c..b1a62e89c4f 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/channel_members_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/channel_members_spec.js @@ -119,7 +119,7 @@ describe('Channel members test', () => { cy.get('#addChannelMembers').click(); // # Enter user1 and user2 emails - cy.findByRole('textbox', {name: 'Search for people or groups'}).typeWithForce(`${user1.email}{enter}${user2.email}{enter}`); + cy.findByRole('combobox', {name: 'Search for people or groups'}).typeWithForce(`${user1.email}{enter}${user2.email}{enter}`); // # Confirm add the users cy.get('#addUsersToChannelModal #saveItems').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/helpers.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/helpers.js index 788b8fb03e1..af70532532a 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/helpers.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/helpers.js @@ -50,7 +50,7 @@ export function makeUserASystemRole(testUsers, role) { cy.findByRole('button', {name: 'Add People'}).click().wait(TIMEOUTS.HALF_SEC); // # Type in user name - cy.findByRole('textbox', {name: 'Search for people'}).typeWithForce(`${testUsers[role].email}`); + cy.findByRole('combobox', {name: 'Search for people'}).typeWithForce(`${testUsers[role].email}`); // # Find the user and click on him cy.get('#multiSelectList').should('be.visible').children().first().click({force: true}); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/user_management/user_management_admin_control_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/user_management/user_management_admin_control_spec.js index 4408cb7eac0..d1eccbb172e 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/user_management/user_management_admin_control_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/user_management/user_management_admin_control_spec.js @@ -135,7 +135,7 @@ describe('User Management', () => { cy.findByRole('button', {name: 'Add People'}).click().wait(TIMEOUTS.HALF_SEC); // # Type in user name - cy.findByRole('textbox', {name: 'Search for people'}).typeWithForce(`${userEmail}`); + cy.findByRole('combobox', {name: 'Search for people'}).typeWithForce(`${userEmail}`); // # Find the user and click on him cy.get('#multiSelectList').should('be.visible').children().first().click({force: true}); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/dm_list_of_users_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/dm_list_of_users_spec.js index e7b114f9635..478279e31ee 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/dm_list_of_users_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/dm_list_of_users_spec.js @@ -44,7 +44,7 @@ describe('Messaging', () => { // # Search for the deactivated user cy.findByRole('dialog', {name: 'Direct Messages'}).should('be.visible').wait(TIMEOUTS.ONE_SEC); - cy.findByRole('textbox', {name: 'Search for people'}).typeWithForce(deactivatedUser.email); + cy.findByRole('combobox', {name: 'Search for people'}).typeWithForce(deactivatedUser.email); // * Verify that the inactive user is not found cy.get('.no-channel-message').should('be.visible').and('contain', 'No results found matching'); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/header_not_cloud_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/header_not_cloud_spec.js index 677d008cbfc..d737f1243bf 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/header_not_cloud_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/header_not_cloud_spec.js @@ -51,7 +51,7 @@ describe('Header', () => { // # Open a DM with the bot cy.uiAddDirectMessage().click().wait(TIMEOUTS.ONE_SEC); cy.findByRole('dialog', {name: 'Direct Messages'}).should('be.visible').wait(TIMEOUTS.ONE_SEC); - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('combobox', {name: 'Search for people'}). typeWithForce('matterpoll').wait(TIMEOUTS.ONE_SEC). typeWithForce('{enter}'); cy.get('#selectItems').contains('matterpoll'); diff --git a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/dm_more_searching_from_page_spec.js b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/dm_more_searching_from_page_spec.js index aebedbddfbb..a0457085272 100644 --- a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/dm_more_searching_from_page_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/dm_more_searching_from_page_spec.js @@ -47,7 +47,7 @@ describe('Multi Team and DM', () => { cy.findByText('Previous').should('exist'); // # Enter a search term - cy.findByRole('textbox', {name: 'Search for people'}).typeWithForce(searchTerm); + cy.findByRole('combobox', {name: 'Search for people'}).typeWithForce(searchTerm); // * Assert that the previous / next links do not appear since there should only be 1 record displayed cy.findByText('Next').should('not.exist'); diff --git a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/dm_more_show_user_count_spec.js b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/dm_more_show_user_count_spec.js index c87f06476cd..35ccbbdde28 100644 --- a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/dm_more_show_user_count_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/dm_more_show_user_count_spec.js @@ -55,7 +55,7 @@ describe('Multi Team and DM', () => { const totalUsers = number.text().split(' ').slice(2, 3); // * Assert that 2 unique users are displayed - cy.findByRole('textbox', {name: 'Search for people'}).typeWithForce(unique).then(() => { + cy.findByRole('combobox', {name: 'Search for people'}).typeWithForce(unique).then(() => { cy.get('#multiSelectList').within(() => { cy.get('.more-modal__details').should('have.length', 2); }); diff --git a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_add_user_spec.js b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_add_user_spec.js index 8cded03fa16..d2055a276d3 100644 --- a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_add_user_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/gm_add_user_spec.js @@ -57,7 +57,7 @@ describe('Multi-user group messages', () => { cy.findByRole('dialog', {name: 'Direct Messages'}).should('be.visible').wait(TIMEOUTS.ONE_SEC); // # Start typing part of a username that matches previously created users - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('combobox', {name: 'Search for people'}). typeWithForce(searchTerm). wait(TIMEOUTS.ONE_SEC); diff --git a/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_deactivation_spec.js b/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_deactivation_spec.js index 7f24904ba74..11a9bab3e24 100644 --- a/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_deactivation_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/system_console/user_management/users_deactivation_spec.js @@ -95,7 +95,7 @@ describe('System Console > User Management > Deactivation', () => { cy.findByRole('dialog', {name: 'Direct Messages'}).should('be.visible').wait(TIMEOUTS.ONE_SEC); // # Start typing part of a username that matches previously created users - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('combobox', {name: 'Search for people'}). typeWithForce(other.username). wait(TIMEOUTS.ONE_SEC); @@ -127,7 +127,7 @@ describe('System Console > User Management > Deactivation', () => { cy.uiAddDirectMessage().click().wait(TIMEOUTS.HALF_SEC); // # Type the user name of user1 on Channel switcher input - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('combobox', {name: 'Search for people'}). typeWithForce(user1.username). wait(TIMEOUTS.ONE_SEC); diff --git a/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js b/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js index dc2fd0d0d04..e1303354496 100644 --- a/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/team_settings/closed_team_invite_by_email_spec.js @@ -85,7 +85,7 @@ describe('Team Settings', () => { cy.get('.InviteAs').findByTestId('inviteMembersLink').click(); } - cy.findByRole('textbox', {name: 'Add or Invite People'}).type(email, {force: true}).wait(TIMEOUTS.HALF_SEC).type('{enter}', {force: true}); + cy.findByRole('combobox', {name: 'Add or Invite People'}).type(email, {force: true}).wait(TIMEOUTS.HALF_SEC).type('{enter}', {force: true}); cy.findByTestId('inviteButton').click(); // # Wait for a while to ensure that email notification is sent and logout from sysadmin account diff --git a/e2e-tests/cypress/tests/integration/channels/team_settings/invite_user_to_closed_team_spec.js b/e2e-tests/cypress/tests/integration/channels/team_settings/invite_user_to_closed_team_spec.js index d9577a58d85..9b629e9a9fc 100644 --- a/e2e-tests/cypress/tests/integration/channels/team_settings/invite_user_to_closed_team_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/team_settings/invite_user_to_closed_team_spec.js @@ -82,7 +82,7 @@ describe('Team Settings', () => { function inviteNewMemberToTeam(email) { cy.wait(TIMEOUTS.HALF_SEC); - cy.findByRole('textbox', {name: 'Add or Invite People'}). + cy.findByRole('combobox', {name: 'Add or Invite People'}). typeWithForce(email). wait(TIMEOUTS.HALF_SEC). typeWithForce('{enter}'); diff --git a/e2e-tests/cypress/tests/support/ui/channel.js b/e2e-tests/cypress/tests/support/ui/channel.js index 635c0acfe66..9b4de2238fb 100644 --- a/e2e-tests/cypress/tests/support/ui/channel.js +++ b/e2e-tests/cypress/tests/support/ui/channel.js @@ -96,7 +96,7 @@ Cypress.Commands.add('goToDm', (username) => { // # Start typing part of a username that matches previously created users cy.get('#selectItems input').typeWithForce(username); cy.findByRole('dialog', {name: 'Direct Messages'}).should('be.visible').wait(TIMEOUTS.ONE_SEC); - cy.findByRole('textbox', {name: 'Search for people'}). + cy.findByRole('combobox', {name: 'Search for people'}). typeWithForce(username). wait(TIMEOUTS.ONE_SEC). typeWithForce('{enter}'); diff --git a/e2e-tests/cypress/tests/support/ui/data_retention.js b/e2e-tests/cypress/tests/support/ui/data_retention.js index 0d8f23c87f2..fdc8e20bd15 100644 --- a/e2e-tests/cypress/tests/support/ui/data_retention.js +++ b/e2e-tests/cypress/tests/support/ui/data_retention.js @@ -28,7 +28,7 @@ Cypress.Commands.add('uiFillOutCustomPolicyFields', (name, durationDropdown, dur Cypress.Commands.add('uiAddTeamsToCustomPolicy', (teamNames) => { cy.uiGetButton('Add teams').click(); teamNames.forEach((teamName) => { - cy.findByRole('textbox', {name: 'Search and add teams'}).typeWithForce(teamName); + cy.findByRole('combobox', {name: 'Search and add teams'}).typeWithForce(teamName); cy.get('.team-info-block').then((el) => { el.click(); }); @@ -39,7 +39,7 @@ Cypress.Commands.add('uiAddTeamsToCustomPolicy', (teamNames) => { Cypress.Commands.add('uiAddChannelsToCustomPolicy', (channelNames) => { cy.uiGetButton('Add channels').click(); channelNames.forEach((channelName) => { - cy.findByRole('textbox', {name: 'Search and add channels'}).typeWithForce(channelName); + cy.findByRole('combobox', {name: 'Search and add channels'}).typeWithForce(channelName); cy.wait(TIMEOUTS.ONE_SEC); cy.get('.channel-info-block').then((el) => { el.click(); diff --git a/e2e-tests/cypress/tests/support/ui_commands.ts b/e2e-tests/cypress/tests/support/ui_commands.ts index d6cf35744ae..4cf093365f2 100644 --- a/e2e-tests/cypress/tests/support/ui_commands.ts +++ b/e2e-tests/cypress/tests/support/ui_commands.ts @@ -237,7 +237,7 @@ function uiGotoDirectMessageWithUser(user: User) { cy.findByRole('dialog', {name: 'Direct Messages'}).should('be.visible').wait(TIMEOUTS.ONE_SEC); // # Type username - cy.findByRole('textbox', {name: 'Search for people'}).click({force: true}). + cy.findByRole('combobox', {name: 'Search for people'}).click({force: true}). type(user.username, {force: true}).wait(TIMEOUTS.ONE_SEC); // * Expect user count in the list to be 1 diff --git a/webapp/channels/package.json b/webapp/channels/package.json index 26ee8d9ce0b..a6872655e71 100644 --- a/webapp/channels/package.json +++ b/webapp/channels/package.json @@ -75,7 +75,7 @@ "react-popper": "2.3.0", "react-redux": "7.2.4", "react-router-dom": "5.3.4", - "react-select": "3.0.3", + "react-select": "5.9.0", "react-transition-group": "4.4.5", "react-virtualized-auto-sizer": "1.0.7", "react-window": "1.8.8", @@ -130,7 +130,6 @@ "@types/react-overlays": "1.1.3", "@types/react-redux": "7.1.31", "@types/react-router-dom": "5.3.3", - "@types/react-select": "3.0.19", "@types/react-transition-group": "4.4.5", "@types/react-virtualized-auto-sizer": "1.0.1", "@types/react-window": "1.8.5", diff --git a/webapp/channels/src/components/admin_console/__snapshots__/schema_admin_settings.test.tsx.snap b/webapp/channels/src/components/admin_console/__snapshots__/schema_admin_settings.test.tsx.snap index 019501bff1a..97bd0e04b2e 100644 --- a/webapp/channels/src/components/admin_console/__snapshots__/schema_admin_settings.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/__snapshots__/schema_admin_settings.test.tsx.snap @@ -422,7 +422,7 @@ exports[`components/admin_console/SchemaAdminSettings should match snapshot with id="SecondSettings.settingj" key="Config_language_SecondSettings.settingj" label="label-j" - noResultText="no-result-j" + noOptionsMessage="no-result-j" onChange={[Function]} selected={Array []} setByEnv={false} diff --git a/webapp/channels/src/components/admin_console/data_retention_settings/data_retention_settings.tsx b/webapp/channels/src/components/admin_console/data_retention_settings/data_retention_settings.tsx index 5a210b9436e..f9769a8370e 100644 --- a/webapp/channels/src/components/admin_console/data_retention_settings/data_retention_settings.tsx +++ b/webapp/channels/src/components/admin_console/data_retention_settings/data_retention_settings.tsx @@ -5,6 +5,7 @@ import React, {createRef} from 'react'; import type {RefObject} from 'react'; import type {WrappedComponentProps} from 'react-intl'; import {FormattedMessage, defineMessages, injectIntl} from 'react-intl'; +import type {SelectInstance} from 'react-select'; import ReactSelect from 'react-select'; import type {AdminConfig} from '@mattermost/types/config'; @@ -81,7 +82,7 @@ export const searchableStrings = [ ]; class DataRetentionSettings extends React.PureComponent { - inputRef: RefObject>; + inputRef: RefObject>; constructor(props: Props) { super(props); this.inputRef = createRef(); diff --git a/webapp/channels/src/components/admin_console/data_retention_settings/global_policy_form/global_policy_form.tsx b/webapp/channels/src/components/admin_console/data_retention_settings/global_policy_form/global_policy_form.tsx index d03bbb78e38..3cc7c758f24 100644 --- a/webapp/channels/src/components/admin_console/data_retention_settings/global_policy_form/global_policy_form.tsx +++ b/webapp/channels/src/components/admin_console/data_retention_settings/global_policy_form/global_policy_form.tsx @@ -214,7 +214,7 @@ export default class GlobalPolicyForm extends React.PureComponent inputValue={this.state.messageRetentionInputValue} width={90} exceptionToInput={[FOREVER]} - disabled={this.isMessageRetentionSetByEnv()} + isDisabled={this.isMessageRetentionSetByEnv()} defaultValue={keepForeverOption()} options={[hoursOption(), daysOption(), yearsOption(), keepForeverOption()]} legend={messages.channelAndMessageRetention} @@ -242,7 +242,7 @@ export default class GlobalPolicyForm extends React.PureComponent inputValue={this.state.fileRetentionInputValue} width={90} exceptionToInput={[FOREVER]} - disabled={this.isFileRetentionSetByEnv()} + isDisabled={this.isFileRetentionSetByEnv()} defaultValue={keepForeverOption()} options={[hoursOption(), daysOption(), yearsOption(), keepForeverOption()]} legend={messages.fileRetention} diff --git a/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.tsx b/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.tsx index 1b8f6ce504f..62905e564db 100644 --- a/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.tsx +++ b/webapp/channels/src/components/admin_console/filter/team_filter_dropdown/team_filter_dropdown.tsx @@ -3,7 +3,7 @@ import React, {useEffect, useState} from 'react'; import {useIntl} from 'react-intl'; -import type {ActionMeta, OptionsType, ValueType} from 'react-select'; +import type {ActionMeta, Options, OnChangeValue} from 'react-select'; import AsyncSelect from 'react-select/async'; import type {PagedTeamSearchOpts, Team} from '@mattermost/types/teams'; @@ -28,7 +28,7 @@ export interface Props extends PropsFromRedux { function TeamFilterDropdown(props: Props) { const {formatMessage} = useIntl(); - const [list, setList] = useState>([]); + const [list, setList] = useState>([]); const [pageNumber, setPageNumber] = useState(0); async function loadListInPageNumber(page: number) { @@ -55,7 +55,7 @@ function TeamFilterDropdown(props: Props) { } } - async function searchInList(term: string, callBack: (options: OptionsType<{label: string; value: string}>) => void) { + async function searchInList(term: string) { try { const response = await props.searchTeams(term, {page: 0, per_page: TEAMS_PER_PAGE} as PagedTeamSearchOpts); if (response && response.data && response.data.teams && response.data.teams.length > 0) { @@ -64,13 +64,13 @@ function TeamFilterDropdown(props: Props) { label: team.display_name, })); - callBack(teams); + return teams; } - callBack([]); + return []; } catch (error) { console.error(error); // eslint-disable-line no-console - callBack([]); + return []; } } @@ -78,7 +78,7 @@ function TeamFilterDropdown(props: Props) { loadListInPageNumber(pageNumber); } - function handleOnChange(value: ValueType, actionMeta: ActionMeta) { + function handleOnChange(value: OnChangeValue, actionMeta: ActionMeta) { if (!actionMeta.action) { return; } diff --git a/webapp/channels/src/components/admin_console/list_table/list_table.tsx b/webapp/channels/src/components/admin_console/list_table/list_table.tsx index d6921c686aa..ba67ec4e84c 100644 --- a/webapp/channels/src/components/admin_console/list_table/list_table.tsx +++ b/webapp/channels/src/components/admin_console/list_table/list_table.tsx @@ -10,7 +10,7 @@ import type {DropResult} from 'react-beautiful-dnd'; import {DragDropContext, Draggable, Droppable} from 'react-beautiful-dnd'; import {FormattedMessage, defineMessages, useIntl} from 'react-intl'; import ReactSelect, {components} from 'react-select'; -import type {IndicatorContainerProps, ValueType} from 'react-select'; +import type {IndicatorsContainerProps, OnChangeValue} from 'react-select'; import {DragVerticalIcon} from '@mattermost/compass-icons/components'; @@ -107,7 +107,7 @@ export function ListTable( const selectedPageSize = pageSizeOptions.find((option) => option.value === props.table.getState().pagination.pageSize) || pageSizeOptions[0]; - function handlePageSizeChange(selectedOption: ValueType) { + function handlePageSizeChange(selectedOption: OnChangeValue) { const {value} = selectedOption as PageSizeOption; props.table.setPageSize(Number(value)); } @@ -370,7 +370,7 @@ export function ListTable( ); } -function IndicatorsContainer(props: IndicatorContainerProps) { +function IndicatorsContainer(props: IndicatorsContainerProps) { return ( diff --git a/webapp/channels/src/components/admin_console/multiselect_settings.tsx b/webapp/channels/src/components/admin_console/multiselect_settings.tsx index 54f7504d905..f213226e3a6 100644 --- a/webapp/channels/src/components/admin_console/multiselect_settings.tsx +++ b/webapp/channels/src/components/admin_console/multiselect_settings.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React, {useState, useCallback, useMemo} from 'react'; -import type {ValueType} from 'react-select'; +import type {OnChangeValue} from 'react-select'; import ReactSelect from 'react-select'; import FormError from 'components/form_error'; @@ -23,7 +23,7 @@ interface Props { disabled?: boolean; setByEnv: boolean; helpText?: React.ReactNode; - noResultText?: React.ReactNode; + noOptionsMessage?: React.ReactNode; } const getOptionLabel = ({text}: { text: string}) => text; @@ -37,11 +37,11 @@ const MultiSelectSetting: React.FC = ({ disabled = false, setByEnv, helpText, - noResultText, + noOptionsMessage, }) => { const [error, setError] = useState(false); - const handleChange = useCallback((newValue: ValueType