From dc47df4e0f338b63624e3aaeab59239db829cdc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 4 Nov 2020 18:54:20 +0100 Subject: [PATCH 01/11] Migrate to typescript Apps related files --- actions/admin_actions.jsx | 2 +- actions/channel_actions.jsx | 4 +- actions/command.js | 2 +- actions/command.test.js | 4 +- ...actions.test.js => global_actions.test.ts} | 36 ++-- ...{global_actions.jsx => global_actions.tsx} | 74 ++++--- actions/post_actions.jsx | 1 + actions/views/create_comment.test.jsx | 2 +- actions/views/posts.js | 2 + actions/views/{rhs.test.js => rhs.test.ts} | 66 +++--- actions/views/{rhs.js => rhs.ts} | 131 +++++++----- actions/websocket_actions.jsx | 2 +- client/web_websocket_client.jsx | 2 +- ...socket_client.jsx => websocket_client.tsx} | 46 +++-- .../admin_navbar_dropdown.tsx | 2 +- .../system_users/system_users.jsx | 2 +- .../system_users_dropdown.tsx | 2 +- components/analytics/team_analytics/index.js | 2 +- .../user_guide_dropdown.test.jsx | 2 +- .../user_guide_dropdown.tsx | 2 +- .../channel_identifier_router/actions.test.ts | 4 +- .../channel_identifier_router/actions.ts | 2 +- components/create_comment/create_comment.jsx | 2 +- components/create_post/create_post.jsx | 2 +- components/create_post/create_post.test.jsx | 4 +- .../do_verify_email/do_verify_email.tsx | 2 +- ...u.test.jsx.snap => dot_menu.test.tsx.snap} | 0 ....jsx.snap => dot_menu_empty.test.tsx.snap} | 0 ...jsx.snap => dot_menu_mobile.test.tsx.snap} | 0 .../{dot_menu.test.jsx => dot_menu.test.tsx} | 16 +- .../dot_menu/{dot_menu.jsx => dot_menu.tsx} | 190 ++++++++++-------- ...empty.test.jsx => dot_menu_empty.test.tsx} | 11 +- ...bile.test.jsx => dot_menu_mobile.test.tsx} | 11 +- components/dot_menu/{index.js => index.ts} | 25 ++- .../edit_post_modal/edit_post_modal.test.jsx | 2 +- components/leave_team_modal/index.ts | 2 +- .../dropdown/sidebar_header_dropdown.jsx | 2 +- .../sidebar_channel/sidebar_channel.jsx | 2 +- .../sidebar_channel/sidebar_channel.test.jsx | 6 +- components/logged_in/logged_in.jsx | 2 +- .../login_controller/login_controller.jsx | 2 +- components/main_menu/main_menu.jsx | 2 +- components/mfa/confirm.jsx | 2 +- components/mfa/confirm.test.jsx | 4 +- .../mfa/mfa_controller/mfa_controller.jsx | 2 +- components/needs_team/needs_team.test.tsx | 2 +- components/needs_team/needs_team.tsx | 2 +- .../action_menu/action_menu.tsx | 6 +- .../post_add_channel_member.test.tsx | 2 +- components/post_view/post_list/index.js | 2 +- components/post_view/post_time/post_time.jsx | 2 +- .../profile_popover/profile_popover.jsx | 2 +- components/rhs_card/rhs_card.jsx | 2 +- components/root/root.jsx | 4 +- components/root/root.test.jsx | 2 +- components/select_team/select_team.test.tsx | 4 +- components/select_team/select_team.tsx | 2 +- .../sidebar_base_channel.tsx | 2 +- .../sidebar_right_menu/sidebar_right_menu.jsx | 2 +- .../signup_controller/signup_controller.jsx | 2 +- .../signup_controller.test.jsx | 2 +- .../signup/signup_email/signup_email.jsx | 2 +- .../terms_of_service/terms_of_service.jsx | 2 +- .../terms_of_service.test.jsx | 4 +- components/textbox/textbox.tsx | 3 +- .../tutorial_intro_screen.test.jsx | 2 +- .../advanced/user_settings_advanced.jsx | 2 +- .../advanced/user_settings_advanced.test.jsx | 2 +- components/widgets/settings/text_setting.tsx | 2 +- package-lock.json | 24 ++- package.json | 1 + ...snap => channel_header_plug.test.tsx.snap} | 0 ....test.jsx => channel_header_plug.test.tsx} | 26 ++- ...eader_plug.jsx => channel_header_plug.tsx} | 85 ++++---- .../{index.js => index.ts} | 6 +- plugins/mobile_channel_header_plug/index.js | 2 +- .../mobile_channel_header_plug.test.jsx | 2 +- .../{browser_store.jsx => browser_store.tsx} | 37 ++-- tests/helpers/client-test-helper.jsx | 2 +- types/store/index.ts | 130 +----------- types/store/plugins.ts | 9 +- types/store/rhs.ts | 2 +- types/store/views.ts | 136 +++++++++++++ 83 files changed, 696 insertions(+), 508 deletions(-) rename actions/{global_actions.test.js => global_actions.test.ts} (95%) rename actions/{global_actions.jsx => global_actions.tsx} (81%) rename actions/views/{rhs.test.js => rhs.test.ts} (92%) rename actions/views/{rhs.js => rhs.ts} (69%) rename client/{websocket_client.jsx => websocket_client.tsx} (78%) rename components/dot_menu/__snapshots__/{dot_menu.test.jsx.snap => dot_menu.test.tsx.snap} (100%) rename components/dot_menu/__snapshots__/{dot_menu_empty.test.jsx.snap => dot_menu_empty.test.tsx.snap} (100%) rename components/dot_menu/__snapshots__/{dot_menu_mobile.test.jsx.snap => dot_menu_mobile.test.tsx.snap} (100%) rename components/dot_menu/{dot_menu.test.jsx => dot_menu.test.tsx} (93%) rename components/dot_menu/{dot_menu.jsx => dot_menu.tsx} (78%) rename components/dot_menu/{dot_menu_empty.test.jsx => dot_menu_empty.test.tsx} (82%) rename components/dot_menu/{dot_menu_mobile.test.jsx => dot_menu_mobile.test.tsx} (82%) rename components/dot_menu/{index.js => index.ts} (74%) rename plugins/channel_header_plug/__snapshots__/{channel_header_plug.test.jsx.snap => channel_header_plug.test.tsx.snap} (100%) rename plugins/channel_header_plug/{channel_header_plug.test.jsx => channel_header_plug.test.tsx} (71%) rename plugins/channel_header_plug/{channel_header_plug.jsx => channel_header_plug.tsx} (78%) rename plugins/channel_header_plug/{index.js => index.ts} (73%) rename stores/{browser_store.jsx => browser_store.tsx} (80%) create mode 100644 types/store/views.ts diff --git a/actions/admin_actions.jsx b/actions/admin_actions.jsx index 5fa3e6d4bc25..db7845649609 100644 --- a/actions/admin_actions.jsx +++ b/actions/admin_actions.jsx @@ -9,7 +9,7 @@ import {bindClientFunc} from 'mattermost-redux/actions/helpers'; import {trackEvent} from 'actions/telemetry_actions.jsx'; -import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx'; +import {emitUserLoggedOutEvent} from 'actions/global_actions'; import {getOnNavigationConfirmed} from 'selectors/views/admin'; import store from 'stores/redux_store.jsx'; import {ActionTypes} from 'utils/constants'; diff --git a/actions/channel_actions.jsx b/actions/channel_actions.jsx index 634467c0b321..639f32cceb14 100644 --- a/actions/channel_actions.jsx +++ b/actions/channel_actions.jsx @@ -116,7 +116,7 @@ export function autocompleteChannels(term, success, error) { const state = getState(); const teamId = getCurrentTeamId(state); if (!teamId) { - return; + return {data: false}; } const {data, error: err} = await dispatch(ChannelActions.autocompleteChannels(teamId, term)); @@ -125,6 +125,8 @@ export function autocompleteChannels(term, success, error) { } else if (err && error) { error({id: err.server_error_id, ...err}); } + + return {data: true}; }; } diff --git a/actions/command.js b/actions/command.js index 5edaa6e43644..b2c5ae3d904a 100644 --- a/actions/command.js +++ b/actions/command.js @@ -10,7 +10,7 @@ import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/sele import {IntegrationTypes} from 'mattermost-redux/action_types'; import {openModal} from 'actions/views/modals'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import * as PostActions from 'actions/post_actions.jsx'; import {isUrlSafe, getSiteURL} from 'utils/url'; diff --git a/actions/command.test.js b/actions/command.test.js index e10a0dafb9ae..5871042a9cea 100644 --- a/actions/command.test.js +++ b/actions/command.test.js @@ -12,7 +12,7 @@ import * as Teams from 'mattermost-redux/selectors/entities/teams'; import {ActionTypes, Constants} from 'utils/constants'; import * as UserAgent from 'utils/user_agent'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import * as Utils from 'utils/utils.jsx'; import UserSettingsModal from 'components/user_settings/modal'; @@ -60,7 +60,7 @@ const initialState = { }; jest.mock('utils/user_agent'); -jest.mock('actions/global_actions.jsx'); +jest.mock('actions/global_actions'); describe('executeCommand', () => { let store; diff --git a/actions/global_actions.test.js b/actions/global_actions.test.ts similarity index 95% rename from actions/global_actions.test.js rename to actions/global_actions.test.ts index 090084857f1c..71e4b9713f4e 100644 --- a/actions/global_actions.test.js +++ b/actions/global_actions.test.ts @@ -3,13 +3,15 @@ import configureStore from 'redux-mock-store'; +import {UserProfile} from 'mattermost-redux/types/users'; +import {Team} from 'mattermost-redux/types/teams'; + import {browserHistory} from 'utils/browser_history'; import {closeRightHandSide, closeMenu as closeRhsMenu} from 'actions/views/rhs'; import {close as closeLhs} from 'actions/views/lhs'; import LocalStorageStore from 'stores/local_storage_store'; -import {getState} from 'stores/redux_store'; - -import {redirectUserToDefaultTeam, toggleSideBarRightMenuAction, getTeamRedirectChannelIfIsAccesible} from 'actions/global_actions.jsx'; +import reduxStore from 'stores/redux_store'; +import {redirectUserToDefaultTeam, toggleSideBarRightMenuAction, getTeamRedirectChannelIfIsAccesible} from 'actions/global_actions'; jest.mock('actions/views/rhs', () => ({ closeMenu: jest.fn(), @@ -60,7 +62,7 @@ describe('actions/global_actions', () => { }, }); - getState.mockImplementation(store.getState); + reduxStore.getState.mockImplementation(store.getState); browserHistory.push = jest.fn(); await redirectUserToDefaultTeam(); @@ -136,7 +138,7 @@ describe('actions/global_actions', () => { }, }); - getState.mockImplementation(store.getState); + reduxStore.getState.mockImplementation(store.getState); browserHistory.push = jest.fn(); await redirectUserToDefaultTeam(); @@ -211,7 +213,7 @@ describe('actions/global_actions', () => { }, }); - getState.mockImplementation(store.getState); + reduxStore.getState.mockImplementation(store.getState); browserHistory.push = jest.fn(); await redirectUserToDefaultTeam(); @@ -285,7 +287,7 @@ describe('actions/global_actions', () => { }, }); - getState.mockImplementation(store.getState); + reduxStore.getState.mockImplementation(store.getState); browserHistory.push = jest.fn(); await redirectUserToDefaultTeam(); @@ -319,7 +321,7 @@ describe('actions/global_actions', () => { }, }); - getState.mockImplementation(store.getState); + reduxStore.getState.mockImplementation(store.getState); browserHistory.push = jest.fn(); await redirectUserToDefaultTeam(); @@ -414,12 +416,12 @@ describe('actions/global_actions', () => { }, }, }); - getState.mockImplementation(store.getState); + reduxStore.getState.mockImplementation(store.getState); LocalStorageStore.setPreviousTeamId(userId, teamId); LocalStorageStore.setPreviousChannelName(userId, teamId, directChannelId); - const result = await getTeamRedirectChannelIfIsAccesible({id: userId}, {id: teamId}); - expect(result.id).toBe(directChannelId); + const result = await getTeamRedirectChannelIfIsAccesible({id: userId} as UserProfile, {id: teamId} as Team); + expect(result?.id).toBe(directChannelId); }); it('should redirect to group message if that\'s the most recently used', async () => { @@ -512,12 +514,12 @@ describe('actions/global_actions', () => { }, }, }); - getState.mockImplementation(store.getState); + reduxStore.getState.mockImplementation(store.getState); LocalStorageStore.setPreviousTeamId(userId, teamId); LocalStorageStore.setPreviousChannelName(userId, teamId, groupChannelId); - const result = await getTeamRedirectChannelIfIsAccesible({id: userId}, {id: teamId}); - expect(result.id).toBe(groupChannelId); + const result = await getTeamRedirectChannelIfIsAccesible({id: userId} as UserProfile, {id: teamId} as Team); + expect(result?.id).toBe(groupChannelId); }); it('should redirect to last channel on first team when current team is no longer available', async () => { @@ -575,7 +577,7 @@ describe('actions/global_actions', () => { }, }); - getState.mockImplementation(store.getState); + reduxStore.getState.mockImplementation(store.getState); browserHistory.push = jest.fn(); await redirectUserToDefaultTeam(); @@ -584,7 +586,9 @@ describe('actions/global_actions', () => { }); test('toggleSideBarRightMenuAction', () => { - const dispatchMock = () => {}; + const dispatchMock = async () => { + return {data: true}; + }; toggleSideBarRightMenuAction()(dispatchMock); expect(closeRhsMenu).toHaveBeenCalled(); expect(closeRightHandSide).toHaveBeenCalled(); diff --git a/actions/global_actions.jsx b/actions/global_actions.tsx similarity index 81% rename from actions/global_actions.jsx rename to actions/global_actions.tsx index 0c2b39acb4a8..aa9e174ba299 100644 --- a/actions/global_actions.jsx +++ b/actions/global_actions.tsx @@ -16,6 +16,10 @@ import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMembershi import {getCurrentUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels} from 'mattermost-redux/selectors/entities/channels'; import {ChannelTypes} from 'mattermost-redux/action_types'; +import {Channel, ChannelMembership} from 'mattermost-redux/types/channels'; +import {UserProfile} from 'mattermost-redux/types/users'; +import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; +import {Team} from 'mattermost-redux/types/teams'; import {browserHistory} from 'utils/browser_history'; import {handleNewPost} from 'actions/post_actions.jsx'; @@ -28,7 +32,7 @@ import * as WebsocketActions from 'actions/websocket_actions.jsx'; import AppDispatcher from 'dispatcher/app_dispatcher.jsx'; import {getCurrentLocale} from 'selectors/i18n'; import {getIsRhsOpen, getRhsState} from 'selectors/rhs'; -import BrowserStore from 'stores/browser_store.jsx'; +import BrowserStore from 'stores/browser_store'; import store from 'stores/redux_store.jsx'; import LocalStorageStore from 'stores/local_storage_store'; import WebSocketClient from 'client/web_websocket_client.jsx'; @@ -43,19 +47,19 @@ import {openModal} from './views/modals'; const dispatch = store.dispatch; const getState = store.getState; -export function emitChannelClickEvent(channel) { - async function userVisitedFakeChannel(chan, success, fail) { +export function emitChannelClickEvent(channel: Channel) { + async function userVisitedFakeChannel(chan: Channel, success: (received: Channel) => void, fail: () => void) { const state = getState(); const currentUserId = getCurrentUserId(state); const otherUserId = Utils.getUserIdFromChannelName(chan); - const {data: receivedChannel} = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState); - if (receivedChannel) { - success(receivedChannel); + const res = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState); + if ('data' in res) { + success(res.data); } else { fail(); } } - function switchToChannel(chan) { + function switchToChannel(chan: Channel) { const state = getState(); const userId = getCurrentUserId(state); const teamId = chan.team_id || getCurrentTeamId(state); @@ -100,7 +104,7 @@ export function emitChannelClickEvent(channel) { switchToChannel(data); }, () => { - browserHistory.push('/' + this.state.currentTeam.name); + browserHistory.push('/'); }, ); } else { @@ -108,11 +112,11 @@ export function emitChannelClickEvent(channel) { } } -export function updateNewMessagesAtInChannel(channelId, last_viewed_at = Date.now()) { +export function updateNewMessagesAtInChannel(channelId: string, lastViewedAt = Date.now()) { return { type: ActionTypes.UPDATE_CHANNEL_LAST_VIEWED_AT, channel_id: channelId, - last_viewed_at, + last_viewed_at: lastViewedAt, }; } @@ -127,7 +131,7 @@ export function toggleShortcutsModal() { }); } -export function showChannelPurposeUpdateModal(channel) { +export function showChannelPurposeUpdateModal(channel: Channel) { AppDispatcher.handleViewAction({ type: ActionTypes.TOGGLE_CHANNEL_PURPOSE_UPDATE_MODAL, value: true, @@ -135,7 +139,7 @@ export function showChannelPurposeUpdateModal(channel) { }); } -export function showChannelNameUpdateModal(channel) { +export function showChannelNameUpdateModal(channel: Channel) { AppDispatcher.handleViewAction({ type: ActionTypes.TOGGLE_CHANNEL_NAME_UPDATE_MODAL, value: true, @@ -143,7 +147,7 @@ export function showChannelNameUpdateModal(channel) { }); } -export function showGetPublicLinkModal(fileId) { +export function showGetPublicLinkModal(fileId: string) { AppDispatcher.handleViewAction({ type: ActionTypes.TOGGLE_GET_PUBLIC_LINK_MODAL, value: true, @@ -158,14 +162,14 @@ export function showGetTeamInviteLinkModal() { }); } -export function showLeavePrivateChannelModal(channel) { +export function showLeavePrivateChannelModal(channel: Channel) { AppDispatcher.handleViewAction({ type: ActionTypes.TOGGLE_LEAVE_PRIVATE_CHANNEL_MODAL, value: channel, }); } -export function showMobileSubMenuModal(elements) { +export function showMobileSubMenuModal(elements: any[]) { // TODO Use more specific type const submenuModalData = { ModalId: ModalIdentifiers.MOBILE_SUBMENU, dialogType: SubMenuModal, @@ -177,7 +181,7 @@ export function showMobileSubMenuModal(elements) { dispatch(openModal(submenuModalData)); } -export function sendEphemeralPost(message, channelId, parentId) { +export function sendEphemeralPost(message: string, channelId: string, parentId: string) { const timestamp = Utils.getTimestamp(); const post = { id: Utils.generateId(), @@ -195,7 +199,7 @@ export function sendEphemeralPost(message, channelId, parentId) { dispatch(handleNewPost(post)); } -export function sendAddToChannelEphemeralPost(user, addedUsername, addedUserId, channelId, postRootId = '', timestamp) { +export function sendAddToChannelEphemeralPost(user: UserProfile, addedUsername: string, addedUserId: string, channelId: string, postRootId = '', timestamp: number) { const post = { id: Utils.generateId(), user_id: user.id, @@ -217,16 +221,19 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, addedUserId, } let lastTimeTypingSent = 0; -export function emitLocalUserTypingEvent(channelId, parentPostId) { - const userTyping = async (actionDispatch, actionGetState) => { +export function emitLocalUserTypingEvent(channelId: string, parentPostId: string) { + const userTyping = async (actionDispatch: DispatchFunc, actionGetState: GetStateFunc) => { const state = actionGetState(); const config = getConfig(state); const t = Date.now(); const stats = getCurrentChannelStats(state); const membersInChannel = stats ? stats.member_count : 0; - if (((t - lastTimeTypingSent) > config.TimeBetweenUserTypingUpdatesMilliseconds) && - (membersInChannel < config.MaxNotificationsPerChannel) && (config.EnableUserTypingMessages === 'true')) { + const timeBetweenUserTypingUpdatesMilliseconds = stringToNumber(config.TimeBetweenUserTypingUpdatesMilliseconds); + const maxNotificationsPerChannel = stringToNumber(config.MaxNotificationsPerChannel); + + if (((t - lastTimeTypingSent) > timeBetweenUserTypingUpdatesMilliseconds) && + (membersInChannel < maxNotificationsPerChannel) && (config.EnableUserTypingMessages === 'true')) { WebSocketClient.userTyping(channelId, parentPostId); lastTimeTypingSent = t; } @@ -237,6 +244,14 @@ export function emitLocalUserTypingEvent(channelId, parentPostId) { return dispatch(userTyping); } +function stringToNumber(s?: string): number { + if (!s) { + return 0; + } + + return parseInt(s, 10); +} + export function emitUserLoggedOutEvent(redirectTo = '/', shouldSignalLogout = true, userAction = true) { // If the logout was intentional, discard knowledge about having previously been logged in. // This bit is otherwise used to detect session expirations on the login page. @@ -262,21 +277,21 @@ export function emitUserLoggedOutEvent(redirectTo = '/', shouldSignalLogout = tr } export function toggleSideBarRightMenuAction() { - return (doDispatch) => { + return (doDispatch: DispatchFunc) => { doDispatch(closeRightHandSide()); doDispatch(closeLhs()); doDispatch(closeRhsMenu()); }; } -export function emitBrowserFocus(focus) { +export function emitBrowserFocus(focus: boolean) { dispatch({ type: ActionTypes.BROWSER_CHANGE_FOCUS, focus, }); } -export async function getTeamRedirectChannelIfIsAccesible(user, team) { +export async function getTeamRedirectChannelIfIsAccesible(user: UserProfile, team: Team) { let state = getState(); let channel = null; @@ -301,7 +316,10 @@ export async function getTeamRedirectChannelIfIsAccesible(user, team) { channel = dmList.find((directChannel) => directChannel.name === channelName); } - let channelMember = getMyChannelMember(state, channel && channel.id); + let channelMember: ChannelMembership | null | undefined; + if (channel) { + channelMember = getMyChannelMember(state, channel.id); + } if (!channel || !channelMember) { // This should be executed in pretty limited scenarios (when the last visited channel in the team has been removed) @@ -350,7 +368,11 @@ export async function redirectUserToDefaultTeam() { return; } - const team = getTeam(state, teamId); + let team: Team | undefined; + if (teamId) { + team = getTeam(state, teamId); + } + if (team && team.delete_at === 0) { const channel = await getTeamRedirectChannelIfIsAccesible(user, team); if (channel) { diff --git a/actions/post_actions.jsx b/actions/post_actions.jsx index 3df083d0dc43..80365444c6ec 100644 --- a/actions/post_actions.jsx +++ b/actions/post_actions.jsx @@ -135,6 +135,7 @@ export function searchForTerm(term) { return (dispatch) => { dispatch(RhsActions.updateSearchTerms(term)); dispatch(RhsActions.showSearchResults()); + return {data: true}; }; } diff --git a/actions/views/create_comment.test.jsx b/actions/views/create_comment.test.jsx index 642c18349536..1b095e7e2fc2 100644 --- a/actions/views/create_comment.test.jsx +++ b/actions/views/create_comment.test.jsx @@ -46,7 +46,7 @@ jest.mock('actions/command', () => ({ executeCommand: jest.fn((...args) => ({type: 'MOCK_ACTIONS_COMMAND_EXECUTE', args})), })); -jest.mock('actions/global_actions.jsx', () => ({ +jest.mock('actions/global_actions', () => ({ emitUserCommentedEvent: jest.fn(), })); diff --git a/actions/views/posts.js b/actions/views/posts.js index c3806cdb8a34..e8ba7d885d91 100644 --- a/actions/views/posts.js +++ b/actions/views/posts.js @@ -34,5 +34,7 @@ export function selectAttachmentMenuAction(postId, actionId, cookie, dataSource, }); dispatch(PostActions.doPostActionWithCookie(postId, actionId, cookie, value)); + + return {data: true}; }; } diff --git a/actions/views/rhs.test.js b/actions/views/rhs.test.ts similarity index 92% rename from actions/views/rhs.test.js rename to actions/views/rhs.test.ts index 8fbba8281d72..b8bbcb943129 100644 --- a/actions/views/rhs.test.js +++ b/actions/views/rhs.test.ts @@ -2,11 +2,15 @@ // See LICENSE.txt for license information. import {batchActions} from 'redux-batched-actions'; -import configureStore from 'redux-mock-store'; +import configureStore, {MockStoreEnhanced} from 'redux-mock-store'; import thunk from 'redux-thunk'; import * as PostActions from 'mattermost-redux/actions/posts'; import * as SearchActions from 'mattermost-redux/actions/search'; import {SearchTypes} from 'mattermost-redux/action_types'; +import {DispatchFunc} from 'mattermost-redux/types/actions'; +import {Post} from 'mattermost-redux/types/posts'; +import {UserProfile} from 'mattermost-redux/types/users'; +import {IDMappedObjects} from 'mattermost-redux/types/utilities'; import { updateRhsState, @@ -29,8 +33,10 @@ import { import {trackEvent} from 'actions/telemetry_actions.jsx'; import {ActionTypes, RHSStates} from 'utils/constants'; import {getBrowserUtcOffset} from 'utils/timezone.jsx'; +import {GlobalState} from 'types/store'; +import {ViewsState} from 'types/store/views'; -const mockStore = configureStore([thunk]); +const mockStore = configureStore([thunk]); const currentChannelId = '123'; const currentTeamId = '321'; @@ -40,7 +46,7 @@ const previousSelectedPost = { id: 'post123', channel_id: 'channel123', root_id: 'root123', -}; +} as Post; const UserSelectors = require('mattermost-redux/selectors/entities/users'); UserSelectors.getCurrentUserMentionKeys = jest.fn(() => [{key: '@here'}, {key: '@mattermost'}, {key: '@channel'}, {key: '@all'}]); @@ -50,13 +56,13 @@ const POST_CREATED_TIME = Date.now(); global.Date.now = jest.fn(() => POST_CREATED_TIME); jest.mock('mattermost-redux/actions/posts', () => ({ - getPostThread: (...args) => ({type: 'MOCK_GET_POST_THREAD', args}), - getProfilesAndStatusesForPosts: (...args) => ({type: 'MOCK_GET_PROFILES_AND_STATUSES_FOR_POSTS', args}), + getPostThread: (...args: any) => ({type: 'MOCK_GET_POST_THREAD', args}), + getProfilesAndStatusesForPosts: (...args: any) => ({type: 'MOCK_GET_PROFILES_AND_STATUSES_FOR_POSTS', args}), })); jest.mock('mattermost-redux/actions/search', () => ({ - searchPostsWithParams: (...args) => ({type: 'MOCK_SEARCH_POSTS', args}), - clearSearch: (...args) => ({type: 'MOCK_CLEAR_SEARCH', args}), + searchPostsWithParams: (...args: any) => ({type: 'MOCK_SEARCH_POSTS', args}), + clearSearch: (...args: any) => ({type: 'MOCK_CLEAR_SEARCH', args}), getFlaggedPosts: jest.fn(), getPinnedPosts: jest.fn(), })); @@ -88,13 +94,13 @@ describe('rhs view actions', () => { automaticTimezone: '', manualTimezone: '', }, - }, - }, + } as UserProfile, + } as IDMappedObjects, }, posts: { posts: { [previousSelectedPost.id]: previousSelectedPost, - }, + } as IDMappedObjects, }, preferences: {myPreferences: {}}, }, @@ -103,9 +109,9 @@ describe('rhs view actions', () => { rhsState: null, }, }, - }; + } as GlobalState; - let store; + let store: MockStoreEnhanced; beforeEach(() => { store = mockStore(initialState); @@ -130,7 +136,7 @@ describe('rhs view actions', () => { id: 'post123', channel_id: 'channel123', root_id: 'root123', - }; + } as Post; test('it dispatches PostActions.getPostThread correctly', () => { store.dispatch(selectPostFromRightHandSideSearch(post)); @@ -149,7 +155,7 @@ describe('rhs view actions', () => { rhs: { rhsState: RHSStates.FLAG, }, - }, + } as ViewsState, }); await store.dispatch(selectPostFromRightHandSideSearch(post)); @@ -192,12 +198,12 @@ describe('rhs view actions', () => { const timeZoneOffset = getBrowserUtcOffset() * 60; const compareStore = mockStore(initialState); - compareStore.dispatch(SearchActions.searchPostsWithParams(currentTeamId, {include_deleted_channels: false, terms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}, true)); + compareStore.dispatch(SearchActions.searchPostsWithParams(currentTeamId, {include_deleted_channels: false, terms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20})); expect(store.getActions()).toEqual(compareStore.getActions()); store.dispatch(performSearch(terms, true)); - compareStore.dispatch(SearchActions.searchPostsWithParams(currentTeamId, {include_deleted_channels: false, terms, is_or_search: true, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}, true)); + compareStore.dispatch(SearchActions.searchPostsWithParams(currentTeamId, {include_deleted_channels: false, terms, is_or_search: true, time_zone_offset: timeZoneOffset, page: 0, per_page: 20})); expect(store.getActions()).toEqual(compareStore.getActions()); }); @@ -213,7 +219,7 @@ describe('rhs view actions', () => { searchTerms: terms, }, }, - }; + } as GlobalState; test('it dispatches the right actions', () => { store = mockStore(testInitialState); @@ -234,7 +240,7 @@ describe('rhs view actions', () => { describe('showFlaggedPosts', () => { test('it dispatches the right actions', async () => { - SearchActions.getFlaggedPosts.mockReturnValue((dispatch) => { + (SearchActions.getFlaggedPosts as jest.Mock).mockReturnValue((dispatch: DispatchFunc) => { dispatch({type: 'MOCK_GET_FLAGGED_POSTS'}); return {data: 'data'}; @@ -278,7 +284,7 @@ describe('rhs view actions', () => { describe('showPinnedPosts', () => { test('it dispatches the right actions for the current channel', async () => { - SearchActions.getPinnedPosts.mockReturnValue((dispatch) => { + (SearchActions.getPinnedPosts as jest.Mock).mockReturnValue((dispatch: DispatchFunc) => { dispatch({type: 'MOCK_GET_PINNED_POSTS'}); return {data: 'data'}; @@ -331,7 +337,7 @@ describe('rhs view actions', () => { test('it dispatches the right actions for a specific channel', async () => { const channelId = 'channel1'; - SearchActions.getPinnedPosts.mockReturnValue((dispatch) => { + (SearchActions.getPinnedPosts as jest.Mock).mockReturnValue((dispatch: DispatchFunc) => { dispatch({type: 'MOCK_GET_PINNED_POSTS'}); return {data: 'data'}; @@ -404,14 +410,14 @@ describe('rhs view actions', () => { }); test('it calls trackEvent correctly', () => { - trackEvent.mockClear(); + (trackEvent as jest.Mock).mockClear(); store.dispatch(showMentions()); expect(trackEvent).toHaveBeenCalledTimes(1); - expect(trackEvent.mock.calls[0][0]).toEqual('api'); - expect(trackEvent.mock.calls[0][1]).toEqual('api_posts_search_mention'); + expect((trackEvent as jest.Mock).mock.calls[0][0]).toEqual('api'); + expect((trackEvent as jest.Mock).mock.calls[0][1]).toEqual('api_posts_search_mention'); }); }); @@ -475,20 +481,20 @@ describe('rhs view actions', () => { ...initialState, views: { rhs: { - state: RHSStates.PLUGIN, + rhsState: RHSStates.PLUGIN, pluggableId, }, }, - }; + } as GlobalState; const stateWithoutPluginRhs = { ...initialState, views: { rhs: { - state: RHSStates.PIN, + rhsState: RHSStates.PIN, }, }, - }; + } as GlobalState; describe('showRHSPlugin', () => { it('dispatches the right action', () => { @@ -625,7 +631,7 @@ describe('rhs view actions', () => { }); it('opens pinned posts', async () => { - SearchActions.getPinnedPosts.mockReturnValue((dispatch) => { + (SearchActions.getPinnedPosts as jest.Mock).mockReturnValue((dispatch: DispatchFunc) => { dispatch({type: 'MOCK_GET_PINNED_POSTS'}); return {data: 'data'}; }); @@ -656,7 +662,7 @@ describe('rhs view actions', () => { }); it('opens flagged posts', async () => { - SearchActions.getFlaggedPosts.mockReturnValue((dispatch) => { + (SearchActions.getFlaggedPosts as jest.Mock).mockReturnValue((dispatch: DispatchFunc) => { dispatch({type: 'MOCK_GET_FLAGGED_POSTS'}); return {data: 'data'}; @@ -732,7 +738,7 @@ describe('rhs view actions', () => { searchTerms: terms, }, }, - }; + } as GlobalState; store = mockStore(searchInitialState); await store.dispatch(openAtPrevious({searchVisible: true})); diff --git a/actions/views/rhs.js b/actions/views/rhs.ts similarity index 69% rename from actions/views/rhs.js rename to actions/views/rhs.ts index a9007523ab1d..38f884b23b60 100644 --- a/actions/views/rhs.js +++ b/actions/views/rhs.ts @@ -18,86 +18,98 @@ import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels' import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {getUserTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; +import {DispatchFunc, GenericAction, GetStateFunc} from 'mattermost-redux/types/actions'; +import {Post} from 'mattermost-redux/types/posts'; import {trackEvent} from 'actions/telemetry_actions.jsx'; import {getSearchTerms, getRhsState, getPluggableId} from 'selectors/rhs'; import {ActionTypes, RHSStates} from 'utils/constants'; import * as Utils from 'utils/utils'; - import {getBrowserUtcOffset, getUtcOffsetForTimeZone} from 'utils/timezone'; +import {RhsState} from 'types/store/rhs'; +import {GlobalState} from 'types/store'; -function selectPostFromRightHandSideSearchWithPreviousState(post, previousRhsState) { - return async (dispatch, getState) => { +function selectPostFromRightHandSideSearchWithPreviousState(post: Post, previousRhsState?: RhsState) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const postRootId = Utils.getRootId(post); await dispatch(PostActions.getPostThread(postRootId)); + const state = getState() as GlobalState; dispatch({ type: ActionTypes.SELECT_POST, postId: postRootId, channelId: post.channel_id, - previousRhsState: previousRhsState || getRhsState(getState()), + previousRhsState: previousRhsState || getRhsState(state), timestamp: Date.now(), }); + + return {data: true}; }; } -function selectPostCardFromRightHandSideSearchWithPreviousState(post, previousRhsState) { - return async (dispatch, getState) => { +function selectPostCardFromRightHandSideSearchWithPreviousState(post: Post, previousRhsState?: RhsState) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState() as GlobalState; + dispatch({ type: ActionTypes.SELECT_POST_CARD, postId: post.id, channelId: post.channel_id, - previousRhsState: previousRhsState || getRhsState(getState()), + previousRhsState: previousRhsState || getRhsState(state), }); + + return {data: true}; }; } -export function updateRhsState(rhsState, channelId) { - return (dispatch, getState) => { +export function updateRhsState(rhsState: string, channelId?: string) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { const action = { type: ActionTypes.UPDATE_RHS_STATE, state: rhsState, - }; + } as GenericAction; if (rhsState === RHSStates.PIN) { action.channelId = channelId || getCurrentChannelId(getState()); } dispatch(action); + + return {data: true}; }; } -export function selectPostFromRightHandSideSearch(post) { +export function selectPostFromRightHandSideSearch(post: Post) { return selectPostFromRightHandSideSearchWithPreviousState(post); } -export function selectPostCardFromRightHandSideSearch(post) { +export function selectPostCardFromRightHandSideSearch(post: Post) { return selectPostCardFromRightHandSideSearchWithPreviousState(post); } -export function selectPostFromRightHandSideSearchByPostId(postId) { - return async (dispatch, getState) => { +export function selectPostFromRightHandSideSearchByPostId(postId: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const post = getPost(getState(), postId); return selectPostFromRightHandSideSearch(post)(dispatch, getState); }; } -export function updateSearchTerms(terms) { +export function updateSearchTerms(terms: string) { return { type: ActionTypes.UPDATE_RHS_SEARCH_TERMS, terms, }; } -function updateSearchResultsTerms(terms) { +function updateSearchResultsTerms(terms: string) { return { type: ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TERMS, terms, }; } -export function performSearch(terms, isMentionSearch) { - return (dispatch, getState) => { +export function performSearch(terms: string, isMentionSearch?: boolean) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { const teamId = getCurrentTeamId(getState()); const config = getConfig(getState()); const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true'; @@ -106,14 +118,16 @@ export function performSearch(terms, isMentionSearch) { const userId = getCurrentUserId(getState()); const userTimezone = getUserTimezone(getState(), userId); const userCurrentTimezone = getUserCurrentTimezone(userTimezone); - const timezoneOffset = (userCurrentTimezone.length > 0 ? getUtcOffsetForTimeZone(userCurrentTimezone) : getBrowserUtcOffset()) * 60; - return dispatch(searchPostsWithParams(teamId, {terms, is_or_search: isMentionSearch, include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}, true)); + const timezoneOffset = ((userCurrentTimezone && (userCurrentTimezone.length > 0)) ? getUtcOffsetForTimeZone(userCurrentTimezone) : getBrowserUtcOffset()) * 60; + return dispatch(searchPostsWithParams(teamId, {terms, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20})); }; } export function showSearchResults(isMentionSearch = false) { - return (dispatch, getState) => { - const searchTerms = getSearchTerms(getState()); + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState() as GlobalState; + + const searchTerms = getSearchTerms(state); if (isMentionSearch) { dispatch(updateRhsState(RHSStates.MENTION)); @@ -126,7 +140,7 @@ export function showSearchResults(isMentionSearch = false) { }; } -export function showRHSPlugin(pluggableId) { +export function showRHSPlugin(pluggableId: string) { const action = { type: ActionTypes.UPDATE_RHS_STATE, state: RHSStates.PLUGIN, @@ -136,27 +150,34 @@ export function showRHSPlugin(pluggableId) { return action; } -export function hideRHSPlugin(pluggableId) { - return (dispatch, getState) => { - if (getPluggableId(getState()) === pluggableId) { +export function hideRHSPlugin(pluggableId: string) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState() as GlobalState; + + if (getPluggableId(state) === pluggableId) { dispatch(closeRightHandSide()); } + + return {data: true}; }; } -export function toggleRHSPlugin(pluggableId) { - return (dispatch, getState) => { - if (getPluggableId(getState()) === pluggableId) { +export function toggleRHSPlugin(pluggableId: string) { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { + const state = getState() as GlobalState; + + if (getPluggableId(state) === pluggableId) { dispatch(hideRHSPlugin(pluggableId)); - return; + return {data: false}; } dispatch(showRHSPlugin(pluggableId)); + return {data: true}; }; } export function showFlaggedPosts() { - return async (dispatch, getState) => { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const teamId = getCurrentTeamId(state); @@ -166,11 +187,15 @@ export function showFlaggedPosts() { }); const results = await dispatch(getFlaggedPosts()); + let data: any; + if ('data' in results) { + data = results.data; + } dispatch(batchActions([ { type: SearchTypes.RECEIVED_SEARCH_POSTS, - data: results.data, + data, }, { type: SearchTypes.RECEIVED_SEARCH_TERM, @@ -181,11 +206,13 @@ export function showFlaggedPosts() { }, }, ])); + + return {data: true}; }; } -export function showPinnedPosts(channelId) { - return async (dispatch, getState) => { +export function showPinnedPosts(channelId?: string) { + return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const state = getState(); const currentChannelId = getCurrentChannelId(state); const teamId = getCurrentTeamId(state); @@ -200,10 +227,15 @@ export function showPinnedPosts(channelId) { const results = await dispatch(getPinnedPosts(channelId || currentChannelId)); + let data: any; + if ('data' in results) { + data = results.data; + } + dispatch(batchActions([ { type: SearchTypes.RECEIVED_SEARCH_POSTS, - data: results.data, + data, }, { type: SearchTypes.RECEIVED_SEARCH_TERM, @@ -214,11 +246,13 @@ export function showPinnedPosts(channelId) { }, }, ])); + + return {data: true}; }; } export function showMentions() { - return (dispatch, getState) => { + return (dispatch: DispatchFunc, getState: GetStateFunc) => { const termKeys = getCurrentUserMentionKeys(getState()).filter(({key}) => { return key !== '@channel' && key !== '@all' && key !== '@here'; }); @@ -238,11 +272,13 @@ export function showMentions() { state: RHSStates.MENTION, }, ])); + + return {data: true}; }; } export function closeRightHandSide() { - return (dispatch) => { + return (dispatch: DispatchFunc) => { dispatch(batchActions([ { type: ActionTypes.UPDATE_RHS_STATE, @@ -255,22 +291,23 @@ export function closeRightHandSide() { timestamp: 0, }, ])); + return {data: true}; }; } -export const toggleMenu = () => (dispatch) => dispatch({ +export const toggleMenu = () => (dispatch: DispatchFunc) => dispatch({ type: ActionTypes.TOGGLE_RHS_MENU, }); -export const openMenu = () => (dispatch) => dispatch({ +export const openMenu = () => (dispatch: DispatchFunc) => dispatch({ type: ActionTypes.OPEN_RHS_MENU, }); -export const closeMenu = () => (dispatch) => dispatch({ +export const closeMenu = () => (dispatch: DispatchFunc) => dispatch({ type: ActionTypes.CLOSE_RHS_MENU, }); -export function setRhsExpanded(expanded) { +export function setRhsExpanded(expanded: boolean) { return { type: ActionTypes.SET_RHS_EXPANDED, expanded, @@ -283,7 +320,7 @@ export function toggleRhsExpanded() { }; } -export function selectPost(post) { +export function selectPost(post: Post) { return { type: ActionTypes.SELECT_POST, postId: post.root_id || post.id, @@ -292,22 +329,24 @@ export function selectPost(post) { }; } -export function selectPostCard(post) { +export function selectPostCard(post: Post) { return {type: ActionTypes.SELECT_POST_CARD, postId: post.id, channelId: post.channel_id}; } export function openRHSSearch() { - return (dispatch) => { + return (dispatch: DispatchFunc) => { dispatch(clearSearch()); dispatch(updateSearchTerms('')); dispatch(updateSearchResultsTerms('')); dispatch(updateRhsState(RHSStates.SEARCH)); + + return {data: true}; }; } -export function openAtPrevious(previous) { - return (dispatch, getState) => { +export function openAtPrevious(previous: any) { // TODO Could not find the proper type. Seems to be in several props around + return (dispatch: DispatchFunc, getState: GetStateFunc) => { if (!previous) { return openRHSSearch()(dispatch); } diff --git a/actions/websocket_actions.jsx b/actions/websocket_actions.jsx index 9d5b9b0da20f..b17bab6b3940 100644 --- a/actions/websocket_actions.jsx +++ b/actions/websocket_actions.jsx @@ -67,7 +67,7 @@ import {syncPostsInChannel} from 'actions/views/channel'; import {browserHistory} from 'utils/browser_history'; import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx'; -import {redirectUserToDefaultTeam} from 'actions/global_actions.jsx'; +import {redirectUserToDefaultTeam} from 'actions/global_actions'; import {handleNewPost} from 'actions/post_actions.jsx'; import * as StatusActions from 'actions/status_actions.jsx'; import {loadProfilesForSidebar} from 'actions/user_actions.jsx'; diff --git a/client/web_websocket_client.jsx b/client/web_websocket_client.jsx index c921ca393ab7..383fddaddf2f 100644 --- a/client/web_websocket_client.jsx +++ b/client/web_websocket_client.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import WebSocketClient from './websocket_client.jsx'; +import WebSocketClient from './websocket_client'; var WebClient = new WebSocketClient(); export default WebClient; diff --git a/client/websocket_client.jsx b/client/websocket_client.tsx similarity index 78% rename from client/websocket_client.jsx rename to client/websocket_client.tsx index 02dbfe20536a..b1ca10d6f000 100644 --- a/client/websocket_client.jsx +++ b/client/websocket_client.tsx @@ -6,6 +6,19 @@ const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins export default class WebSocketClient { + private conn: WebSocket | null; + private connectionUrl: string | null; + private sequence: number; + private eventSequence: number; + private connectFailCount: number; + private eventCallback: ((msg: any) => void) | null; + private responseCallbacks: {[x:number]: ((msg: any) => void)}; + private firstConnectCallback: (() => void) | null; + private reconnectCallback: (() => void) | null; + private missedEventCallback: (() => void) | null; + private errorCallback: ((event: Event) => void) | null; + private closeCallback: ((connectFailCount: number) => void) | null; + constructor() { this.conn = null; this.connectionUrl = null; @@ -21,7 +34,7 @@ export default class WebSocketClient { this.closeCallback = null; } - initialize(connectionUrl = this.connectionUrl, token) { + initialize(connectionUrl = this.connectionUrl, token?: string) { if (this.conn) { return; } @@ -122,27 +135,27 @@ export default class WebSocketClient { }; } - setEventCallback(callback) { + setEventCallback(callback: (msg: any) => void) { this.eventCallback = callback; } - setFirstConnectCallback(callback) { + setFirstConnectCallback(callback: () => void) { this.firstConnectCallback = callback; } - setReconnectCallback(callback) { + setReconnectCallback(callback: () => void) { this.reconnectCallback = callback; } - setMissedEventCallback(callback) { + setMissedEventCallback(callback: () => void) { this.missedEventCallback = callback; } - setErrorCallback(callback) { + setErrorCallback(callback: (event: Event) => void) { this.errorCallback = callback; } - setCloseCallback(callback) { + setCloseCallback(callback: (connectFailCount: number) => void) { this.closeCallback = callback; } @@ -157,7 +170,7 @@ export default class WebSocketClient { } } - sendMessage(action, data, responseCallback) { + sendMessage(action: string, data: any, responseCallback?: () => void) { // TODO data any or unknown? const msg = { action, seq: this.sequence++, @@ -176,15 +189,18 @@ export default class WebSocketClient { } } - userTyping(channelId, parentId, callback) { - const data = {}; + userTyping(channelId: string, parentId: string, callback?: () => void) { + const data = {} as { + channel_id: string, + parent_id: string, + }; // TODO is this a defined type? data.channel_id = channelId; data.parent_id = parentId; this.sendMessage('user_typing', data, callback); } - userUpdateActiveStatus(userIsActive, manual, callback) { + userUpdateActiveStatus(userIsActive: boolean, manual: boolean, callback?: () => void) { const data = { user_is_active: userIsActive, manual, @@ -192,12 +208,14 @@ export default class WebSocketClient { this.sendMessage('user_update_active_status', data, callback); } - getStatuses(callback) { + getStatuses(callback?: () => void) { this.sendMessage('get_statuses', null, callback); } - getStatusesByIds(userIds, callback) { - const data = {}; + getStatusesByIds(userIds: string[], callback?: () => void) { + const data = {} as { + user_ids: string[], + }; // TODO is this a defined type? data.user_ids = userIds; this.sendMessage('get_statuses_by_ids', data, callback); } diff --git a/components/admin_console/admin_navbar_dropdown/admin_navbar_dropdown.tsx b/components/admin_console/admin_navbar_dropdown/admin_navbar_dropdown.tsx index a730897db47b..c94f976b5053 100644 --- a/components/admin_console/admin_navbar_dropdown/admin_navbar_dropdown.tsx +++ b/components/admin_console/admin_navbar_dropdown/admin_navbar_dropdown.tsx @@ -6,7 +6,7 @@ import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'; import {Team} from 'mattermost-redux/types/teams'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import {trackEvent} from 'actions/telemetry_actions.jsx'; import {filterAndSortTeamsByDisplayName} from 'utils/team_utils.jsx'; diff --git a/components/admin_console/system_users/system_users.jsx b/components/admin_console/system_users/system_users.jsx index 49c74911a585..81e3f3dc0705 100644 --- a/components/admin_console/system_users/system_users.jsx +++ b/components/admin_console/system_users/system_users.jsx @@ -18,7 +18,7 @@ import FormattedAdminHeader from 'components/widgets/admin_console/formatted_adm import FormattedMarkdownMessage from 'components/formatted_markdown_message.jsx'; import SystemPermissionGate from 'components/permissions_gates/system_permission_gate'; import ConfirmModal from 'components/confirm_modal'; -import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx'; +import {emitUserLoggedOutEvent} from 'actions/global_actions'; import SystemUsersList from './list'; diff --git a/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.tsx b/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.tsx index 4723466bdb9e..8f044f596933 100644 --- a/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.tsx +++ b/components/admin_console/system_users/system_users_dropdown/system_users_dropdown.tsx @@ -17,7 +17,7 @@ import {Constants} from 'utils/constants'; import * as Utils from 'utils/utils.jsx'; import {t} from 'utils/i18n'; import {getSiteURL} from 'utils/url'; -import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx'; +import {emitUserLoggedOutEvent} from 'actions/global_actions'; import ConfirmModal from 'components/confirm_modal'; import SystemPermissionGate from 'components/permissions_gates/system_permission_gate'; diff --git a/components/analytics/team_analytics/index.js b/components/analytics/team_analytics/index.js index eb84f9d1bb6e..cc4a4d30db30 100644 --- a/components/analytics/team_analytics/index.js +++ b/components/analytics/team_analytics/index.js @@ -7,7 +7,7 @@ import {getTeams} from 'mattermost-redux/actions/teams'; import {getProfilesInTeam} from 'mattermost-redux/actions/users'; import {getTeamsList} from 'mattermost-redux/selectors/entities/teams'; -import BrowserStore from 'stores/browser_store.jsx'; +import BrowserStore from 'stores/browser_store'; import {getCurrentLocale} from 'selectors/i18n'; import TeamAnalytics from './team_analytics.jsx'; diff --git a/components/channel_header/components/user_guide_dropdown/user_guide_dropdown.test.jsx b/components/channel_header/components/user_guide_dropdown/user_guide_dropdown.test.jsx index 36d989c23014..998d0c0e8b7b 100644 --- a/components/channel_header/components/user_guide_dropdown/user_guide_dropdown.test.jsx +++ b/components/channel_header/components/user_guide_dropdown/user_guide_dropdown.test.jsx @@ -9,7 +9,7 @@ import Menu from 'components/widgets/menu/menu'; import {trackEvent} from 'actions/telemetry_actions.jsx'; import UserGuideDropdown from 'components/channel_header/components/user_guide_dropdown/user_guide_dropdown'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; jest.mock('actions/global_actions', () => ({ toggleShortcutsModal: jest.fn(), diff --git a/components/channel_header/components/user_guide_dropdown/user_guide_dropdown.tsx b/components/channel_header/components/user_guide_dropdown/user_guide_dropdown.tsx index dbc4a0e7c338..fcf0b68189c6 100644 --- a/components/channel_header/components/user_guide_dropdown/user_guide_dropdown.tsx +++ b/components/channel_header/components/user_guide_dropdown/user_guide_dropdown.tsx @@ -10,7 +10,7 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper'; import UserGuideIcon from 'components/widgets/icons/user_guide_icon'; import Menu from 'components/widgets/menu/menu'; import OverlayTrigger from 'components/overlay_trigger'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import {trackEvent} from 'actions/telemetry_actions.jsx'; const askTheCommunityUrl = 'https://mattermost.com/pl/default-ask-mattermost-community/'; diff --git a/components/channel_layout/channel_identifier_router/actions.test.ts b/components/channel_layout/channel_identifier_router/actions.test.ts index 12b193e50072..94a8b3c8834e 100644 --- a/components/channel_layout/channel_identifier_router/actions.test.ts +++ b/components/channel_layout/channel_identifier_router/actions.test.ts @@ -7,7 +7,7 @@ import configureStore from 'redux-mock-store'; import {joinChannel} from 'mattermost-redux/actions/channels'; import {getUserByEmail} from 'mattermost-redux/actions/users'; -import {emitChannelClickEvent} from 'actions/global_actions.jsx'; +import {emitChannelClickEvent} from 'actions/global_actions'; import { goToChannelByChannelName, goToDirectChannelByUserId, @@ -17,7 +17,7 @@ import { getPathFromIdentifier, } from 'components/channel_layout/channel_identifier_router/actions'; -jest.mock('actions/global_actions.jsx', () => ({ +jest.mock('actions/global_actions', () => ({ emitChannelClickEvent: jest.fn(), })); diff --git a/components/channel_layout/channel_identifier_router/actions.ts b/components/channel_layout/channel_identifier_router/actions.ts index 7e53abeaf14b..196897bda088 100644 --- a/components/channel_layout/channel_identifier_router/actions.ts +++ b/components/channel_layout/channel_identifier_router/actions.ts @@ -13,7 +13,7 @@ import {History} from 'history'; import {Constants} from 'utils/constants'; import {openDirectChannelToUserId} from 'actions/channel_actions'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import * as Utils from 'utils/utils.jsx'; import {Match, MatchAndHistory} from './channel_identifier_router'; diff --git a/components/create_comment/create_comment.jsx b/components/create_comment/create_comment.jsx index b66e324f120b..7d74a9595b3b 100644 --- a/components/create_comment/create_comment.jsx +++ b/components/create_comment/create_comment.jsx @@ -10,7 +10,7 @@ import {FormattedMessage, injectIntl} from 'react-intl'; import {sortFileInfos} from 'mattermost-redux/utils/file_utils'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import Constants, {Locations} from 'utils/constants'; import {intlShape} from 'utils/react_intl'; diff --git a/components/create_post/create_post.jsx b/components/create_post/create_post.jsx index 26a8d34eb7a5..e538b4be6924 100644 --- a/components/create_post/create_post.jsx +++ b/components/create_post/create_post.jsx @@ -9,7 +9,7 @@ import {FormattedMessage, injectIntl} from 'react-intl'; import {Posts} from 'mattermost-redux/constants'; import {sortFileInfos} from 'mattermost-redux/utils/file_utils'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import Constants, {StoragePrefixes, ModalIdentifiers, Locations, A11yClassNames} from 'utils/constants'; import {t} from 'utils/i18n'; import { diff --git a/components/create_post/create_post.test.jsx b/components/create_post/create_post.test.jsx index 352fc99257d5..2f5a1eebc784 100644 --- a/components/create_post/create_post.test.jsx +++ b/components/create_post/create_post.test.jsx @@ -7,7 +7,7 @@ import {Posts} from 'mattermost-redux/constants'; import {shallowWithIntl} from 'tests/helpers/intl-test-helper'; import {testComponentForLineBreak} from 'tests/helpers/line_break_helpers'; import {testComponentForMarkdownHotkeys, makeSelectionEvent} from 'tests/helpers/markdown_hotkey_helpers.js'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import EmojiMap from 'utils/emoji_map'; import Constants, {StoragePrefixes, ModalIdentifiers} from 'utils/constants'; @@ -17,7 +17,7 @@ import CreatePost from 'components/create_post/create_post.jsx'; import FileUpload from 'components/file_upload'; import Textbox from 'components/textbox'; -jest.mock('actions/global_actions.jsx', () => ({ +jest.mock('actions/global_actions', () => ({ emitLocalUserTypingEvent: jest.fn(), emitUserPostedEvent: jest.fn(), showChannelNameUpdateModal: jest.fn(), diff --git a/components/do_verify_email/do_verify_email.tsx b/components/do_verify_email/do_verify_email.tsx index f505f8f5d0dd..1ef22db4d4c3 100644 --- a/components/do_verify_email/do_verify_email.tsx +++ b/components/do_verify_email/do_verify_email.tsx @@ -15,7 +15,7 @@ import logoImage from 'images/logo.png'; import BackButton from 'components/common/back_button'; import LoadingScreen from 'components/loading_screen'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; type Props = { location: { diff --git a/components/dot_menu/__snapshots__/dot_menu.test.jsx.snap b/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap similarity index 100% rename from components/dot_menu/__snapshots__/dot_menu.test.jsx.snap rename to components/dot_menu/__snapshots__/dot_menu.test.tsx.snap diff --git a/components/dot_menu/__snapshots__/dot_menu_empty.test.jsx.snap b/components/dot_menu/__snapshots__/dot_menu_empty.test.tsx.snap similarity index 100% rename from components/dot_menu/__snapshots__/dot_menu_empty.test.jsx.snap rename to components/dot_menu/__snapshots__/dot_menu_empty.test.tsx.snap diff --git a/components/dot_menu/__snapshots__/dot_menu_mobile.test.jsx.snap b/components/dot_menu/__snapshots__/dot_menu_mobile.test.tsx.snap similarity index 100% rename from components/dot_menu/__snapshots__/dot_menu_mobile.test.jsx.snap rename to components/dot_menu/__snapshots__/dot_menu_mobile.test.tsx.snap diff --git a/components/dot_menu/dot_menu.test.jsx b/components/dot_menu/dot_menu.test.tsx similarity index 93% rename from components/dot_menu/dot_menu.test.jsx rename to components/dot_menu/dot_menu.test.tsx index 32a860accca3..24b067d00867 100644 --- a/components/dot_menu/dot_menu.test.jsx +++ b/components/dot_menu/dot_menu.test.tsx @@ -4,9 +4,11 @@ import {shallow} from 'enzyme'; import React from 'react'; +import {Post} from 'mattermost-redux/types/posts'; + import {Locations, PostTypes} from 'utils/constants'; -import DotMenu, {PLUGGABLE_COMPONENT} from './dot_menu'; +import DotMenu, {Props, PLUGGABLE_COMPONENT} from './dot_menu'; jest.mock('utils/utils', () => { const original = jest.requireActual('utils/utils'); @@ -18,7 +20,7 @@ jest.mock('utils/utils', () => { describe('components/dot_menu/DotMenu', () => { const baseProps = { - post: {id: 'post_id_1', is_pinned: false, type: ''}, + post: {id: 'post_id_1', is_pinned: false} as Post, isLicensed: false, postEditTimeLimit: '-1', handleCommentClick: jest.fn(), @@ -27,6 +29,7 @@ describe('components/dot_menu/DotMenu', () => { components: {}, channelIsArchived: false, currentTeamUrl: '', + location: Locations.CENTER, actions: { flagPost: jest.fn(), unflagPost: jest.fn(), @@ -38,14 +41,15 @@ describe('components/dot_menu/DotMenu', () => { }, canEdit: false, canDelete: false, - }; + pluginMenuItems: [], + } as Props; test('should match snapshot, on Center', () => { const props = { ...baseProps, canEdit: true, }; - const wrapper = shallow( + const wrapper = shallow( , ); @@ -99,7 +103,7 @@ describe('components/dot_menu/DotMenu', () => { ...baseProps.post, type: PostTypes.JOIN_CHANNEL, }, - }; + } as Props; const wrapper = shallow( , ); @@ -159,7 +163,7 @@ describe('components/dot_menu/DotMenu', () => { const props = { ...baseProps, location: Locations.SEARCH, - }; + } as Props; const wrapper = shallow( , ); diff --git a/components/dot_menu/dot_menu.jsx b/components/dot_menu/dot_menu.tsx similarity index 78% rename from components/dot_menu/dot_menu.jsx rename to components/dot_menu/dot_menu.tsx index 35a9022717bd..adb075143a2e 100644 --- a/components/dot_menu/dot_menu.jsx +++ b/components/dot_menu/dot_menu.tsx @@ -1,13 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; import {FormattedMessage} from 'react-intl'; import {Tooltip} from 'react-bootstrap'; import Permissions from 'mattermost-redux/constants/permissions'; +import {Post} from 'mattermost-redux/types/posts'; import {Locations, ModalIdentifiers, Constants} from 'utils/constants'; import DeletePostModal from 'components/delete_post_modal'; @@ -16,94 +16,112 @@ import DelayedAction from 'utils/delayed_action'; import * as PostUtils from 'utils/post_utils.jsx'; import * as Utils from 'utils/utils.jsx'; import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; - import Pluggable from 'plugins/pluggable'; - import Menu from 'components/widgets/menu/menu'; import MenuWrapper from 'components/widgets/menu/menu_wrapper'; import DotsHorizontalIcon from 'components/widgets/icons/dots_horizontal'; +import {PluginComponent} from 'types/store/plugins'; const MENU_BOTTOM_MARGIN = 80; export const PLUGGABLE_COMPONENT = 'PostDropdownMenuItem'; -export default class DotMenu extends React.PureComponent { - static propTypes = { - post: PropTypes.object.isRequired, - teamId: PropTypes.string, - location: PropTypes.oneOf([Locations.CENTER, Locations.RHS_ROOT, Locations.RHS_COMMENT, Locations.SEARCH]).isRequired, - commentCount: PropTypes.number, - isFlagged: PropTypes.bool, - handleCommentClick: PropTypes.func, - handleDropdownOpened: PropTypes.func, - handleAddReactionClick: PropTypes.func, - isMenuOpen: PropTypes.bool, - isReadOnly: PropTypes.bool, - pluginMenuItems: PropTypes.arrayOf(PropTypes.object), - isLicensed: PropTypes.bool.isRequired, - postEditTimeLimit: PropTypes.string.isRequired, - enableEmojiPicker: PropTypes.bool.isRequired, - channelIsArchived: PropTypes.bool.isRequired, - currentTeamUrl: PropTypes.string.isRequired, - - /* - * Components for overriding provided by plugins +// TODO: Should substitute locations from contants.jsx +export enum Location { + Center = 'CENTER', + RHSRoot = 'RHS_ROOT', + RHSComment = 'RHS_COMMENT', + Search = 'SEARCH', + Nowhere = 'NO_WHERE', +} + +export type Props = { + post: Post; + teamId?: string; + location: Location; + commentCount?: number; + isFlagged?: boolean; + handleCommentClick?: () => void; + handleDropdownOpened?: () => void; + handleAddReactionClick?: () => void; + isMenuOpen?: boolean; + isReadOnly?: boolean; + pluginMenuItems: PluginComponent[]; + isLicensed: boolean; + postEditTimeLimit?: string; + enableEmojiPicker: boolean; + channelIsArchived: boolean; + currentTeamUrl: string; + canEdit: boolean; + canDelete: boolean; + + /** + * Components for overriding provided by plugins + */ + components: { + [componentName: string]: PluginComponent[], + }, + actions: { + + /** + * Function flag the post + */ + flagPost: (id: string) => void, + + /** + * Function to unflag the post + */ + unflagPost: (id: string) => void, + + /** + * Function to set the editing post + */ + setEditingPost: (postId: string, commentCount: number, refocusId: string, title: string, isRHS: boolean) => void, + + /** + * Function to pin the post + */ + pinPost: (id: string) => void, + + /** + * Function to unpin the post */ - components: PropTypes.object.isRequired, - - actions: PropTypes.shape({ - - /** - * Function flag the post - */ - flagPost: PropTypes.func.isRequired, - - /** - * Function to unflag the post - */ - unflagPost: PropTypes.func.isRequired, - - /** - * Function to set the editing post - */ - setEditingPost: PropTypes.func.isRequired, - - /** - * Function to pin the post - */ - pinPost: PropTypes.func.isRequired, - - /** - * Function to unpin the post - */ - unpinPost: PropTypes.func.isRequired, - - /** - * Function to open a modal - */ - openModal: PropTypes.func.isRequired, - - /* - * Function to set the unread mark at given post - */ - markPostAsUnread: PropTypes.func.isRequired, - }).isRequired, - - canEdit: PropTypes.bool.isRequired, - canDelete: PropTypes.bool.isRequired, + unpinPost: (id: string) => void, + + /** + * Function to open a modal + */ + openModal: (modalData: any) => void, // TODO Improve typing + + /** + * Function to set the unread mark at given post + */ + markPostAsUnread: (post: Post) => void, } +} + +type State = { + openUp: boolean, + width: number, + canEdit: boolean, + canDelete: boolean, +} + +export default class DotMenu extends React.PureComponent { + editDisableAction: DelayedAction + buttonRef: React.RefObject - static defaultProps = { - post: {}, + public static defaultProps: Partial = { + post: {} as Post, commentCount: 0, isFlagged: false, isReadOnly: false, pluginMenuItems: [], - location: Locations.CENTER, + location: Location.Center, enableEmojiPicker: false, } - constructor(props) { + constructor(props: Props) { super(props); this.editDisableAction = new DelayedAction(this.handleEditDisable); @@ -112,19 +130,22 @@ export default class DotMenu extends React.PureComponent { openUp: false, width: 0, canEdit: props.canEdit && !props.isReadOnly, + canDelete: props.canDelete && !props.isReadOnly, }; this.buttonRef = React.createRef(); } disableCanEditPostByTime() { - const {post, isLicensed, postEditTimeLimit} = this.props; + const {post, isLicensed} = this.props; const {canEdit} = this.state; + const postEditTimeLimit = this.props.postEditTimeLimit || String(Constants.UNSET_POST_EDIT_TIME_LIMIT); + if (canEdit && isLicensed) { if (String(postEditTimeLimit) !== String(Constants.UNSET_POST_EDIT_TIME_LIMIT)) { const milliseconds = 1000; - const timeLeft = (post.create_at + (postEditTimeLimit * milliseconds)) - Utils.getTimestamp(); + const timeLeft = (post.create_at + (parseInt(postEditTimeLimit, 10) * milliseconds)) - Utils.getTimestamp(); if (timeLeft > 0) { this.editDisableAction.fireAfter(timeLeft + milliseconds); } @@ -136,7 +157,7 @@ export default class DotMenu extends React.PureComponent { this.disableCanEditPostByTime(); } - static getDerivedStateFromProps(props) { + static getDerivedStateFromProps(props: Props) { return { canEdit: props.canEdit && !props.isReadOnly, canDelete: props.canDelete && !props.isReadOnly, @@ -160,7 +181,7 @@ export default class DotMenu extends React.PureComponent { } // listen to clicks/taps on add reaction menu item and pass to parent handler - handleAddReactionMenuItemActivated = (e) => { + handleAddReactionMenuItemActivated = (e: Event) => { e.preventDefault(); // to be safe, make sure the handler function has been defined @@ -181,12 +202,12 @@ export default class DotMenu extends React.PureComponent { } } - handleUnreadMenuItemActivated = (e) => { + handleUnreadMenuItemActivated = (e: Event) => { e.preventDefault(); this.props.actions.markPostAsUnread(this.props.post); } - handleDeleteMenuItemActivated = (e) => { + handleDeleteMenuItemActivated = (e: Event) => { e.preventDefault(); const deletePostModalData = { @@ -203,9 +224,14 @@ export default class DotMenu extends React.PureComponent { } handleEditMenuItemActivated = () => { + let commentCount = 0; + if (this.props.commentCount) { + commentCount = this.props.commentCount; + } + this.props.actions.setEditingPost( this.props.post.id, - this.props.commentCount, + commentCount, this.props.location === Locations.CENTER ? 'post_textbox' : 'reply_textbox', this.props.post.root_id ? Utils.localizeMessage('rhs_comment.comment', 'Comment') : Utils.localizeMessage('create_post.post', 'Post'), this.props.location === Locations.RHS_ROOT || this.props.location === Locations.RHS_COMMENT, @@ -224,11 +250,11 @@ export default class DotMenu extends React.PureComponent { ) - refCallback = (menuRef) => { + refCallback = (menuRef?: any) => { // TODO use more specific type if (menuRef) { const rect = menuRef.rect(); - const buttonRect = this.buttonRef.current.getBoundingClientRect(); - const y = typeof buttonRect.y === 'undefined' ? buttonRect.top : buttonRect.y; + const buttonRect = this.buttonRef.current?.getBoundingClientRect(); + const y = (typeof buttonRect?.y === 'undefined' ? buttonRect?.top : buttonRect.y) || 0; const windowHeight = window.innerHeight; const totalSpace = windowHeight - MENU_BOTTOM_MARGIN; @@ -242,7 +268,7 @@ export default class DotMenu extends React.PureComponent { } } - renderDivider = (suffix) => { + renderDivider = (suffix: string) => { return (
  • { return { @@ -24,13 +27,14 @@ jest.mock('utils/post_utils', () => { describe('components/dot_menu/DotMenu returning empty ("")', () => { test('should match snapshot, return empty ("") on Center', () => { const baseProps = { - post: {id: 'post_id_1'}, + post: {id: 'post_id_1'} as Post, isLicensed: false, postEditTimeLimit: '-1', enableEmojiPicker: true, components: {}, channelIsArchived: false, currentTeamUrl: '', + location: Locations.CENTER, actions: { flagPost: jest.fn(), unflagPost: jest.fn(), @@ -42,7 +46,8 @@ describe('components/dot_menu/DotMenu returning empty ("")', () => { }, canEdit: false, canDelete: false, - }; + pluginMenuItems: [], + } as Props; const wrapper = shallow( , diff --git a/components/dot_menu/dot_menu_mobile.test.jsx b/components/dot_menu/dot_menu_mobile.test.tsx similarity index 82% rename from components/dot_menu/dot_menu_mobile.test.jsx rename to components/dot_menu/dot_menu_mobile.test.tsx index c03771314c2b..0db4c8f1dc86 100644 --- a/components/dot_menu/dot_menu_mobile.test.jsx +++ b/components/dot_menu/dot_menu_mobile.test.tsx @@ -4,7 +4,10 @@ import {shallow} from 'enzyme'; import React from 'react'; -import DotMenu from 'components/dot_menu/dot_menu.jsx'; +import {Post} from 'mattermost-redux/types/posts'; + +import DotMenu, {Props} from 'components/dot_menu/dot_menu'; +import {Locations} from 'utils/constants'; jest.mock('utils/utils', () => { return { @@ -24,13 +27,14 @@ jest.mock('utils/post_utils', () => { describe('components/dot_menu/DotMenu on mobile view', () => { test('should match snapshot', () => { const baseProps = { - post: {id: 'post_id_1'}, + post: {id: 'post_id_1'} as Post, isLicensed: false, postEditTimeLimit: '-1', enableEmojiPicker: true, components: {}, channelIsArchived: false, currentTeamUrl: '', + location: Locations.CENTER, actions: { flagPost: jest.fn(), unflagPost: jest.fn(), @@ -42,7 +46,8 @@ describe('components/dot_menu/DotMenu on mobile view', () => { }, canEdit: false, canDelete: false, - }; + pluginMenuItems: [], + } as Props; const wrapper = shallow( , diff --git a/components/dot_menu/index.js b/components/dot_menu/index.ts similarity index 74% rename from components/dot_menu/index.js rename to components/dot_menu/index.ts index fa4f25c95034..803a715296db 100644 --- a/components/dot_menu/index.js +++ b/components/dot_menu/index.ts @@ -2,12 +2,14 @@ // See LICENSE.txt for license information. import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; +import {bindActionCreators, Dispatch} from 'redux'; import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general'; import {getChannel} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getCurrentTeamId, getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; +import {GenericAction} from 'mattermost-redux/types/actions'; +import {Post} from 'mattermost-redux/types/posts'; import {openModal} from 'actions/views/modals'; import { @@ -18,14 +20,28 @@ import { setEditingPost, markPostAsUnread, } from 'actions/post_actions.jsx'; +import {GlobalState} from 'types/store'; import * as PostUtils from 'utils/post_utils.jsx'; import {isArchivedChannel} from 'utils/channel_utils'; import {getSiteURL} from 'utils/url'; -import DotMenu from './dot_menu.jsx'; +import DotMenu, {Location} from './dot_menu'; -function mapStateToProps(state, ownProps) { +type OwnProps = { + post: Post; + commentCount?: number; + isFlagged?: boolean; + handleCommentClick?: () => void; + handleDropdownOpened?: () => void; + handleAddReactionClick?: () => void; + isMenuOpen?: boolean; + isReadOnly?: boolean; + enableEmojiPicker: boolean; + location: Location, +} + +function mapStateToProps(state: GlobalState, ownProps: OwnProps) { const {post} = ownProps; const license = getLicense(state); @@ -42,14 +58,13 @@ function mapStateToProps(state, ownProps) { isLicensed: getLicense(state).IsLicensed === 'true', teamId: getCurrentTeamId(state), pluginMenuItems: state.plugins.components.PostDropdownMenu, - shouldShowDotMenu: PostUtils.shouldShowDotMenu(state, post, channel), canEdit: PostUtils.canEditPost(state, post, license, config, channel, userId), canDelete: PostUtils.canDeletePost(state, post, channel), currentTeamUrl, }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch: Dispatch) { return { actions: bindActionCreators({ flagPost, diff --git a/components/edit_post_modal/edit_post_modal.test.jsx b/components/edit_post_modal/edit_post_modal.test.jsx index 3874927b32bf..0c8cd368a7c2 100644 --- a/components/edit_post_modal/edit_post_modal.test.jsx +++ b/components/edit_post_modal/edit_post_modal.test.jsx @@ -14,7 +14,7 @@ import EditPostModal from 'components/edit_post_modal/edit_post_modal.jsx'; import {testComponentForMarkdownHotkeys, makeSelectionEvent} from 'tests/helpers/markdown_hotkey_helpers.js'; import Textbox from 'components/textbox'; -jest.mock('actions/global_actions.jsx', () => ({ +jest.mock('actions/global_actions', () => ({ emitClearSuggestions: jest.fn(), })); diff --git a/components/leave_team_modal/index.ts b/components/leave_team_modal/index.ts index e0bdd2fa7bd4..bffa9825dc55 100644 --- a/components/leave_team_modal/index.ts +++ b/components/leave_team_modal/index.ts @@ -8,7 +8,7 @@ import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {removeUserFromTeam as leaveTeam} from 'mattermost-redux/actions/teams'; import {GenericAction} from 'mattermost-redux/types/actions'; -import {toggleSideBarRightMenuAction} from 'actions/global_actions.jsx'; +import {toggleSideBarRightMenuAction} from 'actions/global_actions'; import {ModalIdentifiers} from 'utils/constants'; import {isModalOpen} from 'selectors/views/modals'; diff --git a/components/legacy_sidebar/header/dropdown/sidebar_header_dropdown.jsx b/components/legacy_sidebar/header/dropdown/sidebar_header_dropdown.jsx index 526d70e33232..6dc8777cce5b 100644 --- a/components/legacy_sidebar/header/dropdown/sidebar_header_dropdown.jsx +++ b/components/legacy_sidebar/header/dropdown/sidebar_header_dropdown.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import React from 'react'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import {Constants, ModalIdentifiers} from 'utils/constants'; import {cmdOrCtrlPressed, isKeyPressed} from 'utils/utils'; diff --git a/components/legacy_sidebar/sidebar_channel/sidebar_channel.jsx b/components/legacy_sidebar/sidebar_channel/sidebar_channel.jsx index 35c357399a2a..fc626aab7a8b 100644 --- a/components/legacy_sidebar/sidebar_channel/sidebar_channel.jsx +++ b/components/legacy_sidebar/sidebar_channel/sidebar_channel.jsx @@ -10,7 +10,7 @@ import {browserHistory} from 'utils/browser_history'; import {Constants} from 'utils/constants'; import {intlShape} from 'utils/react_intl'; import {trackEvent} from 'actions/telemetry_actions.jsx'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import SidebarChannelButtonOrLink from '../sidebar_channel_button_or_link/sidebar_channel_button_or_link.jsx'; import SidebarTutorialTip from '../sidebar_tutorial_tip.jsx'; diff --git a/components/legacy_sidebar/sidebar_channel/sidebar_channel.test.jsx b/components/legacy_sidebar/sidebar_channel/sidebar_channel.test.jsx index 7756019f5406..3c43d1aaba31 100644 --- a/components/legacy_sidebar/sidebar_channel/sidebar_channel.test.jsx +++ b/components/legacy_sidebar/sidebar_channel/sidebar_channel.test.jsx @@ -34,8 +34,8 @@ jest.mock('utils/utils.jsx', () => { }; }); -jest.mock('actions/global_actions.jsx', () => { - const original = jest.requireActual('actions/global_actions.jsx'); +jest.mock('actions/global_actions', () => { + const original = jest.requireActual('actions/global_actions'); return { ...original, showLeavePrivateChannelModal: jest.fn(), @@ -429,7 +429,7 @@ describe('component/legacy_sidebar/sidebar_channel/SidebarChannel', () => { test('should leave the private channel', () => { const trackEvent = require('actions/telemetry_actions.jsx').trackEvent; - const showLeavePrivateChannelModal = require('actions/global_actions.jsx').showLeavePrivateChannelModal; + const showLeavePrivateChannelModal = require('actions/global_actions').showLeavePrivateChannelModal; const props = { ...defaultProps, channelId: 'test-channel-id', diff --git a/components/logged_in/logged_in.jsx b/components/logged_in/logged_in.jsx index 67abae3f8070..b2cf1ea0eb71 100644 --- a/components/logged_in/logged_in.jsx +++ b/components/logged_in/logged_in.jsx @@ -7,7 +7,7 @@ import {Redirect} from 'react-router'; import {viewChannel} from 'mattermost-redux/actions/channels'; import semver from 'semver'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import * as WebSocketActions from 'actions/websocket_actions.jsx'; import * as UserAgent from 'utils/user_agent'; import LoadingScreen from 'components/loading_screen'; diff --git a/components/login/login_controller/login_controller.jsx b/components/login/login_controller/login_controller.jsx index 90c0b22c8eba..add08a99667a 100644 --- a/components/login/login_controller/login_controller.jsx +++ b/components/login/login_controller/login_controller.jsx @@ -8,7 +8,7 @@ import {Link} from 'react-router-dom'; import {Client4} from 'mattermost-redux/client'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import LocalStorageStore from 'stores/local_storage_store'; import {browserHistory} from 'utils/browser_history'; diff --git a/components/main_menu/main_menu.jsx b/components/main_menu/main_menu.jsx index d8ac19691288..bebb9b4721d5 100644 --- a/components/main_menu/main_menu.jsx +++ b/components/main_menu/main_menu.jsx @@ -8,7 +8,7 @@ import {Permissions} from 'mattermost-redux/constants'; import {isEmpty} from 'lodash'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import {Constants, ModalIdentifiers} from 'utils/constants'; import {intlShape} from 'utils/react_intl'; import {cmdOrCtrlPressed, isKeyPressed} from 'utils/utils'; diff --git a/components/mfa/confirm.jsx b/components/mfa/confirm.jsx index f6cc6630e1f0..1a4b9325cc39 100644 --- a/components/mfa/confirm.jsx +++ b/components/mfa/confirm.jsx @@ -7,7 +7,7 @@ import {FormattedMessage} from 'react-intl'; import Constants from 'utils/constants'; import {isKeyPressed} from 'utils/utils.jsx'; -import {redirectUserToDefaultTeam} from 'actions/global_actions.jsx'; +import {redirectUserToDefaultTeam} from 'actions/global_actions'; import FormattedMarkdownMessage from 'components/formatted_markdown_message.jsx'; diff --git a/components/mfa/confirm.test.jsx b/components/mfa/confirm.test.jsx index e72f857d889e..9fd157e53c26 100644 --- a/components/mfa/confirm.test.jsx +++ b/components/mfa/confirm.test.jsx @@ -4,13 +4,13 @@ import React from 'react'; import {shallow} from 'enzyme'; -import {redirectUserToDefaultTeam} from 'actions/global_actions.jsx'; +import {redirectUserToDefaultTeam} from 'actions/global_actions'; import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import Confirm from 'components/mfa/confirm.jsx'; import Constants from 'utils/constants'; -jest.mock('actions/global_actions.jsx', () => ({ +jest.mock('actions/global_actions', () => ({ redirectUserToDefaultTeam: jest.fn(), })); diff --git a/components/mfa/mfa_controller/mfa_controller.jsx b/components/mfa/mfa_controller/mfa_controller.jsx index 92b91b902be7..c029b1d130b2 100644 --- a/components/mfa/mfa_controller/mfa_controller.jsx +++ b/components/mfa/mfa_controller/mfa_controller.jsx @@ -6,7 +6,7 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import {Route, Switch} from 'react-router-dom'; -import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx'; +import {emitUserLoggedOutEvent} from 'actions/global_actions'; import logoImage from 'images/logo.png'; import BackButton from 'components/common/back_button'; import LogoutIcon from 'components/widgets/icons/fa_logout_icon'; diff --git a/components/needs_team/needs_team.test.tsx b/components/needs_team/needs_team.test.tsx index 9402f3ed917e..5056f3f3236b 100644 --- a/components/needs_team/needs_team.test.tsx +++ b/components/needs_team/needs_team.test.tsx @@ -7,7 +7,7 @@ import {TeamType} from 'mattermost-redux/types/teams'; import NeedsTeam from 'components/needs_team/needs_team'; -jest.mock('actions/global_actions.jsx', () => ({ +jest.mock('actions/global_actions', () => ({ emitCloseRightHandSide: jest.fn(), })); diff --git a/components/needs_team/needs_team.tsx b/components/needs_team/needs_team.tsx index 08edbb0f7b9a..e51c62a06c35 100644 --- a/components/needs_team/needs_team.tsx +++ b/components/needs_team/needs_team.tsx @@ -12,7 +12,7 @@ import {UserStatus} from 'mattermost-redux/types/users'; import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'actions/status_actions.jsx'; import {startPeriodicSync, stopPeriodicSync, reconnect} from 'actions/websocket_actions.jsx'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import Constants from 'utils/constants'; import * as UserAgent from 'utils/user_agent'; diff --git a/components/post_view/message_attachments/action_menu/action_menu.tsx b/components/post_view/message_attachments/action_menu/action_menu.tsx index a22208fb35b2..2c5badd6da60 100644 --- a/components/post_view/message_attachments/action_menu/action_menu.tsx +++ b/components/post_view/message_attachments/action_menu/action_menu.tsx @@ -3,7 +3,7 @@ import React from 'react'; -import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; +import {ActionResult, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {UserProfile} from 'mattermost-redux/types/users'; import {Channel} from 'mattermost-redux/types/channels'; import {ServerError} from 'mattermost-redux/types/errors'; @@ -23,7 +23,7 @@ type AutocompleteChannelsAction = ( term: string, success: (channels: Channel[]) => void, error: (error: Error) => void -) => (dispatch: DispatchFunc, getState: GetStateFunc) => Promise; +) => (dispatch: DispatchFunc, getState: GetStateFunc) => Promise; type SelectAttachmentMenuAction = ( postId: string, @@ -32,7 +32,7 @@ type SelectAttachmentMenuAction = ( dataSource: string | undefined, text: string, value: string -) => (dispatch: DispatchFunc) => Promise; +) => (dispatch: DispatchFunc) => Promise; type Option = { text: string; diff --git a/components/post_view/post_add_channel_member/post_add_channel_member.test.tsx b/components/post_view/post_add_channel_member/post_add_channel_member.test.tsx index 6da7c1abc71d..9499335d57c6 100644 --- a/components/post_view/post_add_channel_member/post_add_channel_member.test.tsx +++ b/components/post_view/post_add_channel_member/post_add_channel_member.test.tsx @@ -11,7 +11,7 @@ import {sendAddToChannelEphemeralPost} from 'actions/global_actions'; import {TestHelper} from 'utils/test_helper'; import PostAddChannelMember, {Props} from 'components/post_view/post_add_channel_member/post_add_channel_member'; -jest.mock('actions/global_actions.jsx', () => { +jest.mock('actions/global_actions', () => { return { sendAddToChannelEphemeralPost: jest.fn(), }; diff --git a/components/post_view/post_list/index.js b/components/post_view/post_list/index.js index c1ecb8e4980b..8735d7b134a4 100644 --- a/components/post_view/post_list/index.js +++ b/components/post_view/post_list/index.js @@ -11,7 +11,7 @@ import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/c import {makePreparePostIdsForPostList} from 'mattermost-redux/utils/post_list'; import {RequestStatus} from 'mattermost-redux/constants'; -import {updateNewMessagesAtInChannel} from 'actions/global_actions.jsx'; +import {updateNewMessagesAtInChannel} from 'actions/global_actions'; import {getLatestPostId, makeCreateAriaLabelForPost} from 'utils/post_utils.jsx'; import { checkAndSetMobileView, diff --git a/components/post_view/post_time/post_time.jsx b/components/post_view/post_time/post_time.jsx index d115905804cb..c2a59e401fc9 100644 --- a/components/post_view/post_time/post_time.jsx +++ b/components/post_view/post_time/post_time.jsx @@ -7,7 +7,7 @@ import {Link} from 'react-router-dom'; import {Tooltip} from 'react-bootstrap'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import {isMobile} from 'utils/user_agent'; import {Locations} from 'utils/constants'; import {isMobile as isMobileView} from 'utils/utils.jsx'; diff --git a/components/profile_popover/profile_popover.jsx b/components/profile_popover/profile_popover.jsx index 628416e8f4c0..49fc232b3e0d 100644 --- a/components/profile_popover/profile_popover.jsx +++ b/components/profile_popover/profile_popover.jsx @@ -12,7 +12,7 @@ import Timestamp from 'components/timestamp'; import OverlayTrigger from 'components/overlay_trigger'; import UserSettingsModal from 'components/user_settings/modal'; import {browserHistory} from 'utils/browser_history'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import Constants, {ModalIdentifiers, UserStatuses} from 'utils/constants'; import {t} from 'utils/i18n'; import {intlShape} from 'utils/react_intl'; diff --git a/components/rhs_card/rhs_card.jsx b/components/rhs_card/rhs_card.jsx index 5d849ee69b89..8efd0c35e3cb 100644 --- a/components/rhs_card/rhs_card.jsx +++ b/components/rhs_card/rhs_card.jsx @@ -14,7 +14,7 @@ import RhsCardHeader from 'components/rhs_card_header'; import Markdown from 'components/markdown'; import UserProfile from 'components/user_profile'; import PostProfilePicture from 'components/post_profile_picture'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; export function renderView(props) { return ( diff --git a/components/root/root.jsx b/components/root/root.jsx index e395480b6da5..a714136d885c 100644 --- a/components/root/root.jsx +++ b/components/root/root.jsx @@ -14,8 +14,8 @@ import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import * as UserAgent from 'utils/user_agent'; import {EmojiIndicesByAlias} from 'utils/emoji.jsx'; import {trackLoadTime} from 'actions/telemetry_actions.jsx'; -import * as GlobalActions from 'actions/global_actions.jsx'; -import BrowserStore from 'stores/browser_store.jsx'; +import * as GlobalActions from 'actions/global_actions'; +import BrowserStore from 'stores/browser_store'; import {loadRecentlyUsedCustomEmojis} from 'actions/emoji_actions.jsx'; import {initializePlugins} from 'plugins'; import 'plugins/export.js'; diff --git a/components/root/root.test.jsx b/components/root/root.test.jsx index 66bdfbcacaae..1940b83c3b44 100644 --- a/components/root/root.test.jsx +++ b/components/root/root.test.jsx @@ -7,7 +7,7 @@ import {shallow} from 'enzyme'; import {Client4} from 'mattermost-redux/client'; import Root from 'components/root/root'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import * as Utils from 'utils/utils'; import Constants, {StoragePrefixes} from 'utils/constants'; diff --git a/components/select_team/select_team.test.tsx b/components/select_team/select_team.test.tsx index 5e87ac145fcc..3dbf4c59877a 100644 --- a/components/select_team/select_team.test.tsx +++ b/components/select_team/select_team.test.tsx @@ -8,9 +8,9 @@ import {Team} from 'mattermost-redux/types/teams'; import SelectTeam, {TEAMS_PER_PAGE} from 'components/select_team/select_team'; -import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx'; +import {emitUserLoggedOutEvent} from 'actions/global_actions'; -jest.mock('actions/global_actions.jsx', () => ({ +jest.mock('actions/global_actions', () => ({ emitUserLoggedOutEvent: jest.fn(), })); diff --git a/components/select_team/select_team.tsx b/components/select_team/select_team.tsx index bbf192a3f133..6839f0080c2c 100644 --- a/components/select_team/select_team.tsx +++ b/components/select_team/select_team.tsx @@ -9,7 +9,7 @@ import {Permissions} from 'mattermost-redux/constants'; import {Team} from 'mattermost-redux/types/teams'; -import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx'; +import {emitUserLoggedOutEvent} from 'actions/global_actions'; import {trackEvent} from 'actions/telemetry_actions.jsx'; import * as UserAgent from 'utils/user_agent'; diff --git a/components/sidebar/sidebar_channel/sidebar_base_channel/sidebar_base_channel.tsx b/components/sidebar/sidebar_channel/sidebar_base_channel/sidebar_base_channel.tsx index bf416531cc36..0c5f71702c86 100644 --- a/components/sidebar/sidebar_channel/sidebar_base_channel/sidebar_base_channel.tsx +++ b/components/sidebar/sidebar_channel/sidebar_base_channel/sidebar_base_channel.tsx @@ -32,7 +32,7 @@ export default class SidebarBaseChannel extends React.PureComponent { - GlobalActions.showLeavePrivateChannelModal({id: this.props.channel.id, display_name: this.props.channel.display_name}); + GlobalActions.showLeavePrivateChannelModal({id: this.props.channel.id, display_name: this.props.channel.display_name} as Channel); trackEvent('ui', 'ui_private_channel_x_button_clicked'); } diff --git a/components/sidebar_right_menu/sidebar_right_menu.jsx b/components/sidebar_right_menu/sidebar_right_menu.jsx index 29c7d97ff26a..22f9a5bb5400 100644 --- a/components/sidebar_right_menu/sidebar_right_menu.jsx +++ b/components/sidebar_right_menu/sidebar_right_menu.jsx @@ -7,7 +7,7 @@ import {Link} from 'react-router-dom'; import classNames from 'classnames'; import {CSSTransition} from 'react-transition-group'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import {Constants} from 'utils/constants'; import * as Utils from 'utils/utils.jsx'; import MenuTutorialTip from 'components/tutorial/menu_tutorial_tip'; diff --git a/components/signup/signup_controller/signup_controller.jsx b/components/signup/signup_controller/signup_controller.jsx index bb22f423bd39..fa4ce3fd80a7 100644 --- a/components/signup/signup_controller/signup_controller.jsx +++ b/components/signup/signup_controller/signup_controller.jsx @@ -8,7 +8,7 @@ import {Link} from 'react-router-dom'; import {Client4} from 'mattermost-redux/client'; import {browserHistory} from 'utils/browser_history'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import logoImage from 'images/logo.png'; import AnnouncementBar from 'components/announcement_bar'; import BackButton from 'components/common/back_button'; diff --git a/components/signup/signup_controller/signup_controller.test.jsx b/components/signup/signup_controller/signup_controller.test.jsx index 9d21c34a887a..db2eae7b2fe3 100644 --- a/components/signup/signup_controller/signup_controller.test.jsx +++ b/components/signup/signup_controller/signup_controller.test.jsx @@ -4,7 +4,7 @@ import {shallow} from 'enzyme'; import React from 'react'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import {browserHistory} from 'utils/browser_history'; import {Constants} from 'utils/constants'; diff --git a/components/signup/signup_email/signup_email.jsx b/components/signup/signup_email/signup_email.jsx index 4515dc9cb9a5..ca880bf001b2 100644 --- a/components/signup/signup_email/signup_email.jsx +++ b/components/signup/signup_email/signup_email.jsx @@ -10,7 +10,7 @@ import {Link} from 'react-router-dom'; import {isEmail} from 'mattermost-redux/utils/helpers'; import {trackEvent} from 'actions/telemetry_actions.jsx'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import {browserHistory} from 'utils/browser_history'; import Constants from 'utils/constants'; import * as Utils from 'utils/utils.jsx'; diff --git a/components/terms_of_service/terms_of_service.jsx b/components/terms_of_service/terms_of_service.jsx index 455859648dbf..1fadb4ea91ed 100644 --- a/components/terms_of_service/terms_of_service.jsx +++ b/components/terms_of_service/terms_of_service.jsx @@ -8,7 +8,7 @@ import {FormattedMessage} from 'react-intl'; import {memoizeResult} from 'mattermost-redux/utils/helpers'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; import AnnouncementBar from 'components/announcement_bar'; import LoadingScreen from 'components/loading_screen'; import LoadingSpinner from 'components/widgets/loading/loading_spinner'; diff --git a/components/terms_of_service/terms_of_service.test.jsx b/components/terms_of_service/terms_of_service.test.jsx index 9c8402540390..db5602bbeacb 100644 --- a/components/terms_of_service/terms_of_service.test.jsx +++ b/components/terms_of_service/terms_of_service.test.jsx @@ -6,9 +6,9 @@ import {shallow} from 'enzyme'; import TermsOfService from 'components/terms_of_service/terms_of_service.jsx'; -import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx'; +import {emitUserLoggedOutEvent} from 'actions/global_actions'; -jest.mock('actions/global_actions.jsx', () => ({ +jest.mock('actions/global_actions', () => ({ emitUserLoggedOutEvent: jest.fn(), redirectUserToDefaultTeam: jest.fn(), })); diff --git a/components/textbox/textbox.tsx b/components/textbox/textbox.tsx index c33e52b65d82..20cd740e5b9d 100644 --- a/components/textbox/textbox.tsx +++ b/components/textbox/textbox.tsx @@ -6,6 +6,7 @@ import React, {ChangeEvent, ElementType, FocusEvent, KeyboardEvent, MouseEvent} import {FormattedMessage} from 'react-intl'; import {Channel} from 'mattermost-redux/types/channels'; +import {ActionResult} from 'mattermost-redux/types/actions'; import AutosizeTextarea from 'components/autosize_textarea'; import PostMarkdown from 'components/post_markdown'; @@ -51,7 +52,7 @@ type Props = { autocompleteGroups: { id: string }[] | null; actions: { autocompleteUsersInChannel: (prefix: string, channelId: string | undefined) => (dispatch: any, getState: any) => Promise; - autocompleteChannels: (term: string, success: (channels: Channel[]) => void, error: () => void) => (dispatch: any, getState: any) => Promise; + autocompleteChannels: (term: string, success: (channels: Channel[]) => void, error: () => void) => (dispatch: any, getState: any) => Promise; searchAssociatedGroupsForReference: (prefix: string, teamId: string, channelId: string | undefined) => (dispatch: any, getState: any) => Promise<{ data: any }>; }; useChannelMentions: boolean; diff --git a/components/tutorial/tutorial_intro_screens/tutorial_intro_screen.test.jsx b/components/tutorial/tutorial_intro_screens/tutorial_intro_screen.test.jsx index e0436637e5bc..9419cbf95f6e 100644 --- a/components/tutorial/tutorial_intro_screens/tutorial_intro_screen.test.jsx +++ b/components/tutorial/tutorial_intro_screens/tutorial_intro_screen.test.jsx @@ -10,7 +10,7 @@ import {Constants, Preferences} from 'utils/constants'; describe('components/tutorial/tutorial_intro_screens/TutorialIntroScreens', () => { jest.mock('actions/telemetry_actions.jsx'); - jest.mock('actions/global_actions.jsx'); + jest.mock('actions/global_actions'); const currentUserId = 'currentUserId'; diff --git a/components/user_settings/advanced/user_settings_advanced.jsx b/components/user_settings/advanced/user_settings_advanced.jsx index a573fb0eb4b0..0b92426fae89 100644 --- a/components/user_settings/advanced/user_settings_advanced.jsx +++ b/components/user_settings/advanced/user_settings_advanced.jsx @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {FormattedMessage} from 'react-intl'; -import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx'; +import {emitUserLoggedOutEvent} from 'actions/global_actions'; import Constants from 'utils/constants'; import * as Utils from 'utils/utils.jsx'; import {t} from 'utils/i18n'; diff --git a/components/user_settings/advanced/user_settings_advanced.test.jsx b/components/user_settings/advanced/user_settings_advanced.test.jsx index e81589e3988f..b5fb2e5c2591 100644 --- a/components/user_settings/advanced/user_settings_advanced.test.jsx +++ b/components/user_settings/advanced/user_settings_advanced.test.jsx @@ -7,7 +7,7 @@ import {shallow} from 'enzyme'; import AdvancedSettingsDisplay from 'components/user_settings/advanced/user_settings_advanced.jsx'; import * as Utils from 'utils/utils'; -jest.mock('actions/global_actions.jsx'); +jest.mock('actions/global_actions'); jest.mock('utils/utils'); describe('components/user_settings/display/UserSettingsDisplay', () => { diff --git a/components/widgets/settings/text_setting.tsx b/components/widgets/settings/text_setting.tsx index 6521f907b6cb..264f25ef606e 100644 --- a/components/widgets/settings/text_setting.tsx +++ b/components/widgets/settings/text_setting.tsx @@ -5,7 +5,7 @@ import React from 'react'; import Setting from './setting'; -type InputTypes = 'input' | 'textarea' | 'number' | 'email' | 'tel' | 'url' | 'password' +export type InputTypes = 'input' | 'textarea' | 'number' | 'email' | 'tel' | 'url' | 'password' export type WidgetTextSettingProps = { id: string; diff --git a/package-lock.json b/package-lock.json index e738a0d6404e..5fe0370684f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4925,6 +4925,14 @@ "@types/react": "*" } }, + "@types/react-overlays": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/react-overlays/-/react-overlays-3.1.0.tgz", + "integrity": "sha512-NzZZHFLj7M7+I+p5rDdVHtm6AeVYQPShVxALiLYhR9leJSX8XujPsYuY+vh7/mzFjv6XR7PxHBAdlFGNaN6QDQ==", + "requires": { + "react-overlays": "*" + } + }, "@types/react-redux": { "version": "7.1.9", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz", @@ -12583,7 +12591,15 @@ "integrity": "sha512-VMtK//85QJomhk3cXOCksNwOYaw1KWnYTS37GYGgyf7A3ajdBoPGhaJuJWAH2S2kq8GZeXkdKn+3Mfmgy11cVw==", "dev": true, "requires": { - "icu4c-data": "0.63.2" + "icu4c-data": "0.64.2" + }, + "dependencies": { + "icu4c-data": { + "version": "0.64.2", + "resolved": "https://registry.npmjs.org/icu4c-data/-/icu4c-data-0.64.2.tgz", + "integrity": "sha512-BPuTfkRTkplmK1pNrqgyOLJ0qB2UcQ12EotVLwiWh4ErtZR1tEYoRZk/LBLmlDfK5v574/lQYLB4jT9vApBiBQ==", + "dev": true + } } }, "function-bind": { @@ -13483,12 +13499,6 @@ "postcss": "^7.0.14" } }, - "icu4c-data": { - "version": "0.63.2", - "resolved": "https://registry.npmjs.org/icu4c-data/-/icu4c-data-0.63.2.tgz", - "integrity": "sha512-vT6/47CcBzDemlhRzkL7dZLoNvuUWjjiuKZHMt5T4dbkKAqLBh7ZQ33GU13ecC/aIcMlIdBOqtF4uIYTgWML4Q==", - "dev": true - }, "identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", diff --git a/package.json b/package.json index 0412b88f80d0..1fe4a9342588 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@stripe/react-stripe-js": "1.1.2", "@stripe/stripe-js": "1.9.0", "@types/country-list": "2.1.0", + "@types/react-overlays": "3.1.0", "bootstrap": "3.4.1", "chart.js": "2.9.3", "compass-mixins": "0.12.10", diff --git a/plugins/channel_header_plug/__snapshots__/channel_header_plug.test.jsx.snap b/plugins/channel_header_plug/__snapshots__/channel_header_plug.test.tsx.snap similarity index 100% rename from plugins/channel_header_plug/__snapshots__/channel_header_plug.test.jsx.snap rename to plugins/channel_header_plug/__snapshots__/channel_header_plug.test.tsx.snap diff --git a/plugins/channel_header_plug/channel_header_plug.test.jsx b/plugins/channel_header_plug/channel_header_plug.test.tsx similarity index 71% rename from plugins/channel_header_plug/channel_header_plug.test.jsx rename to plugins/channel_header_plug/channel_header_plug.test.tsx index 8505df832088..b6321ab83b3d 100644 --- a/plugins/channel_header_plug/channel_header_plug.test.jsx +++ b/plugins/channel_header_plug/channel_header_plug.test.tsx @@ -4,11 +4,15 @@ import React from 'react'; import {mount} from 'enzyme'; -import ChannelHeaderPlug from 'plugins/channel_header_plug/channel_header_plug.jsx'; +import {Channel, ChannelMembership} from 'mattermost-redux/types/channels'; +import {Theme} from 'mattermost-redux/types/preferences'; + +import ChannelHeaderPlug from 'plugins/channel_header_plug/channel_header_plug'; import {mountWithIntl} from '../../tests/helpers/intl-test-helper'; +import {PluginComponent} from 'types/store/plugins'; describe('plugins/ChannelHeaderPlug', () => { - const testPlug = { + const testPlug: PluginComponent = { id: 'someid', pluginId: 'pluginid', icon: , @@ -21,9 +25,9 @@ describe('plugins/ChannelHeaderPlug', () => { const wrapper = mount( , ); expect(wrapper).toMatchSnapshot(); @@ -33,9 +37,9 @@ describe('plugins/ChannelHeaderPlug', () => { const wrapper = mount( , ); expect(wrapper).toMatchSnapshot(); @@ -52,9 +56,9 @@ describe('plugins/ChannelHeaderPlug', () => { {...testPlug, id: 'someid5'}, {...testPlug, id: 'someid6'}, ]} - channel={{}} - channelMember={{}} - theme={{}} + channel={{} as Channel} + channelMember={{} as ChannelMembership} + theme={{} as Theme} />, ); expect(wrapper).toMatchSnapshot(); diff --git a/plugins/channel_header_plug/channel_header_plug.jsx b/plugins/channel_header_plug/channel_header_plug.tsx similarity index 78% rename from plugins/channel_header_plug/channel_header_plug.jsx rename to plugins/channel_header_plug/channel_header_plug.tsx index f057eedebfe2..0a89422ba867 100644 --- a/plugins/channel_header_plug/channel_header_plug.jsx +++ b/plugins/channel_header_plug/channel_header_plug.tsx @@ -3,25 +3,29 @@ /* eslint-disable react/no-multi-comp */ -import PropTypes from 'prop-types'; import React from 'react'; import {Dropdown, Tooltip} from 'react-bootstrap'; import {RootCloseWrapper} from 'react-overlays'; import {FormattedMessage} from 'react-intl'; +import {Channel, ChannelMembership} from 'mattermost-redux/types/channels'; +import {Theme} from 'mattermost-redux/types/preferences'; + import HeaderIconWrapper from 'components/channel_header/components/header_icon_wrapper'; import PluginChannelHeaderIcon from '../../components/widgets/icons/plugin_channel_header_icon'; import {Constants} from 'utils/constants'; import OverlayTrigger from 'components/overlay_trigger'; +import {PluginComponent} from 'types/store/plugins'; + +type CustomMenuProps = { + open?: boolean; + children?: React.ReactNode; + onClose: () => void; + rootCloseEvent?: 'click' | 'mousedown'; + bsRole: string; +} -class CustomMenu extends React.PureComponent { - static propTypes = { - open: PropTypes.bool, - children: PropTypes.node, - onClose: PropTypes.func.isRequired, - rootCloseEvent: PropTypes.oneOf(['click', 'mousedown']), - } - +class CustomMenu extends React.PureComponent { handleRootClose = () => { this.props.onClose(); } @@ -50,15 +54,18 @@ class CustomMenu extends React.PureComponent { } } -class CustomToggle extends React.PureComponent { - static propTypes = { - children: PropTypes.element, - dropdownOpen: PropTypes.bool, - onClick: PropTypes.func, - } +type CustomToggleProps = { + children?: React.ReactNode; + dropdownOpen?: boolean; + onClick?: (e: React.MouseEvent) => void; + bsRole: string; +} - handleClick = (e) => { - this.props.onClick(e); +class CustomToggle extends React.PureComponent { + handleClick = (e: React.MouseEvent) => { + if (this.props.onClick) { + this.props.onClick(e); + } } render() { @@ -82,31 +89,26 @@ class CustomToggle extends React.PureComponent { } } -export default class ChannelHeaderPlug extends React.PureComponent { - static propTypes = { - - /* - * Components or actions to add as channel header buttons - */ - components: PropTypes.array, - - channel: PropTypes.object.isRequired, - channelMember: PropTypes.object.isRequired, +type ChannelHeaderPlugProps = { + components: PluginComponent[]; + channel: Channel; + channelMember: ChannelMembership; + theme: Theme; +} - /* - * Logged in user's theme - */ - theme: PropTypes.object.isRequired, - } +type ChannelHeaderPlugState = { + dropdownOpen: boolean; +} - constructor(props) { +export default class ChannelHeaderPlug extends React.PureComponent { + constructor(props: ChannelHeaderPlugProps) { super(props); this.state = { dropdownOpen: false, }; } - toggleDropdown = (dropdownOpen) => { + toggleDropdown = (dropdownOpen: boolean) => { this.setState({dropdownOpen}); } @@ -114,18 +116,18 @@ export default class ChannelHeaderPlug extends React.PureComponent { this.toggleDropdown(false); } - fireActionAndClose = (action) => { + fireActionAndClose = (action: (...args: any) => void) => { // TODO add more specific types action(this.props.channel, this.props.channelMember); this.onClose(); } - createButton = (plug) => { + createButton = (plug: PluginComponent) => { return ( plug.action(this.props.channel, this.props.channelMember)} + iconComponent={plug.icon!} + onClick={() => plug.action!(this.props.channel, this.props.channelMember)} buttonId={plug.id} tooltipKey={'plugin'} tooltipText={plug.tooltipText ? plug.tooltipText : plug.dropdownText} @@ -133,8 +135,8 @@ export default class ChannelHeaderPlug extends React.PureComponent { ); } - createDropdown = (plugs) => { - const items = plugs.map((plug) => { + createDropdown = (plugs: PluginComponent[]) => { + const items = plugs.filter((plug) => plug.action).map((plug) => { return (
  • this.fireActionAndClose(plug.action)} + onClick={() => this.fireActionAndClose(plug.action!)} > {plug.icon} {plug.dropdownText} @@ -156,7 +158,6 @@ export default class ChannelHeaderPlug extends React.PureComponent { { const testPlug = { diff --git a/stores/browser_store.jsx b/stores/browser_store.tsx similarity index 80% rename from stores/browser_store.jsx rename to stores/browser_store.tsx index 7a763faa9779..fa015274a491 100644 --- a/stores/browser_store.jsx +++ b/stores/browser_store.tsx @@ -12,27 +12,30 @@ const dispatch = store.dispatch; const getState = store.getState; class BrowserStoreClass { - setItem(name, value) { + private hasCheckedLocalStorage?: boolean; + private localStorageSupported?: boolean; + + setItem(name: string, value: string) { dispatch(Actions.setItem(name, value)); } - getItem(name, defaultValue) { + getItem(name: string, defaultValue: string) { return Selectors.makeGetItem(name, defaultValue)(getState()); } - removeItem(name) { + removeItem(name: string) { dispatch(Actions.removeItem(name)); } - setGlobalItem(name, value) { + setGlobalItem(name: string, value: string) { dispatch(Actions.setGlobalItem(name, value)); } - getGlobalItem(name, defaultValue = null) { + getGlobalItem(name: string, defaultValue: string | null = null) { return Selectors.makeGetGlobalItem(name, defaultValue)(getState()); } - removeGlobalItem(name) { + removeGlobalItem(name: string) { dispatch(Actions.removeGlobalItem(name)); } @@ -49,7 +52,7 @@ class BrowserStoreClass { } } - isSignallingLogout(logoutId) { + isSignallingLogout(logoutId: string) { return logoutId === sessionStorage.getItem(StoragePrefixes.LOGOUT); } @@ -64,11 +67,11 @@ class BrowserStoreClass { } } - isSignallingLogin(loginId) { + isSignallingLogin(loginId: string) { return loginId === sessionStorage.getItem(StoragePrefixes.LOGIN); } - clear(options) { + clear(options?: any) { // TODO add more specific types dispatch(Actions.clear(options)); } @@ -84,7 +87,7 @@ class BrowserStoreClass { if (localStorage.getItem('__testLocal__') === '1') { this.localStorageSupported = true; } - localStorage.removeItem('__testLocal__', '1'); + localStorage.removeItem('__testLocal__'); } catch (e) { this.localStorageSupported = false; } @@ -106,26 +109,26 @@ class BrowserStoreClass { return localStorage.getItem(StoragePrefixes.LANDING_PAGE_SEEN); } - setLandingPageSeen(landingPageSeen) { - localStorage.setItem(StoragePrefixes.LANDING_PAGE_SEEN, landingPageSeen); + setLandingPageSeen(landingPageSeen: boolean) { + localStorage.setItem(StoragePrefixes.LANDING_PAGE_SEEN, String(landingPageSeen)); } - getLandingPreference(siteUrl) { + getLandingPreference(siteUrl?: string) { return localStorage.getItem(StoragePrefixes.LANDING_PREFERENCE + String(siteUrl)); } - setLandingPreferenceToMattermostApp(siteUrl) { + setLandingPreferenceToMattermostApp(siteUrl?: string) { localStorage.setItem(StoragePrefixes.LANDING_PREFERENCE + String(siteUrl), LandingPreferenceTypes.MATTERMOSTAPP); } - setLandingPreferenceToBrowser(siteUrl) { + setLandingPreferenceToBrowser(siteUrl?: string) { localStorage.setItem(StoragePrefixes.LANDING_PREFERENCE + String(siteUrl), LandingPreferenceTypes.BROWSER); } - clearLandingPreference(siteUrl) { + clearLandingPreference(siteUrl?: string) { localStorage.removeItem(StoragePrefixes.LANDING_PREFERENCE + String(siteUrl)); } } -var BrowserStore = new BrowserStoreClass(); +const BrowserStore = new BrowserStoreClass(); export default BrowserStore; diff --git a/tests/helpers/client-test-helper.jsx b/tests/helpers/client-test-helper.jsx index 6618aaab07d9..b61b687d3455 100644 --- a/tests/helpers/client-test-helper.jsx +++ b/tests/helpers/client-test-helper.jsx @@ -4,7 +4,7 @@ import jqd from 'jquery-deferred'; import {Client4} from 'mattermost-redux/client'; -import WebSocketClient from 'client/websocket_client.jsx'; +import WebSocketClient from 'client/websocket_client'; var HEADER_TOKEN = 'token'; diff --git a/types/store/index.ts b/types/store/index.ts index d3f0f8dd0854..3d7f6b76bf8d 100644 --- a/types/store/index.ts +++ b/types/store/index.ts @@ -1,14 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Channel} from 'mattermost-redux/types/channels'; -import {MarketplacePlugin} from 'mattermost-redux/types/plugins'; import {GlobalState as BaseGlobalState} from 'mattermost-redux/types/store'; import {Dictionary} from 'mattermost-redux/types/utilities'; -import {I18nState} from './i18n'; -import {RhsViewState} from './rhs'; import {PluginsState} from './plugins'; +import {ViewsState} from './views'; export type DraggingState = { state?: string; @@ -22,128 +19,5 @@ export type GlobalState = BaseGlobalState & { storage: Dictionary; }; - views: { - admin: { - navigationBlock: { - blocked: boolean; - onNavigationConfirmed: () => void; - showNavigationPrompt: boolean; - }; - }; - - browser: { - focused: boolean; - }; - - channel: { - postVisibility: { - [channelId: string]: number; - }; - lastChannelViewTime: { - [channelId: string]: number; - }; - loadingPost: { - [channelId: string]: boolean; - }; - focusedPostId: string; - mobileView: boolean; - lastUnreadChannel: (Channel & {hadMentions: boolean}) | null; // Actually only an object with {id: string, hadMentions: boolean} - lastGetPosts: { - [channelId: string]: number; - }; - channelPrefetchStatus: { - [channelId: string]: string; - }; - }; - - rhs: RhsViewState; - - posts: { - editingPost: { - show: boolean; - }; - menuActions: { - [postId: string]: { - [actionId: string]: { - text: string; - value: string; - }; - }; - }; - }; - - modals: { - [modalId: string]: { - open: boolean; - dialogProps: Dictionary; - dialogType: React.Component; - }; - }; - - emoji: { - emojiPickerCustomPage: 0; - }; - - i18n: I18nState; - - lhs: { - isOpen: boolean; - }; - - search: { - modalSearch: string; - modalFilters: { - roles?: string[]; - channel_roles?: string[]; - team_roles?: string[]; - }; - systemUsersSearch: { - term: string; - team: string; - filter: string; - }; - userGridSearch: { - term: string; - filters: { - roles?: string[]; - channel_roles?: string[]; - team_roles?: string[]; - }; - }; - }; - - notice: { - [noticeType: string]: boolean; - }; - - system: { - websocketConnectErrorCount: number; - }; - - channelSelectorModal: { - channels: Channel[]; - }; - - settings: { - activeSection: string; - previousActiveSection: string; - }; - - marketplace: { - plugins: MarketplacePlugin[]; - installing: {[pluginId: string]: boolean}; - errors: {[pluginId: string]: string}; - filter: string; - }; - - channelSidebar: { - unreadFilterEnabled: boolean; - draggingState: DraggingState; - newCategoryIds: string[]; - }; - - nextSteps: { - show: boolean; - }; - }; + views: ViewsState; }; diff --git a/types/store/plugins.ts b/types/store/plugins.ts index 1a9431e0c6eb..9c4a6e97d484 100644 --- a/types/store/plugins.ts +++ b/types/store/plugins.ts @@ -31,7 +31,14 @@ export type PluginsState = { export type PluginComponent = { id: string; pluginId: string; - component: React.Component; + component?: React.Component; + subMenu?: any[]; // TODO Add more concrete type + text?: string; + dropdownText?: string; + tooltipText?: string; + icon?: React.ReactElement; + filter?: (id: string) => boolean; + action?: (...args: any) => void; // TODO Add more concrete types? }; export type PostPluginComponent = { diff --git a/types/store/rhs.ts b/types/store/rhs.ts index fa4c2c31fd8e..d296e59a80ef 100644 --- a/types/store/rhs.ts +++ b/types/store/rhs.ts @@ -39,4 +39,4 @@ export type RhsViewState = { isMenuOpen: boolean; }; -export type RhsState = 'mention' | 'search' | 'flag' | 'pin' | 'plugin'; +export type RhsState = 'mention' | 'search' | 'flag' | 'pin' | 'plugin' | null; diff --git a/types/store/views.ts b/types/store/views.ts new file mode 100644 index 000000000000..d6b5fe08f04c --- /dev/null +++ b/types/store/views.ts @@ -0,0 +1,136 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Channel} from 'mattermost-redux/types/channels'; +import {MarketplacePlugin} from 'mattermost-redux/types/plugins'; +import {Dictionary} from 'mattermost-redux/types/utilities'; + +import {I18nState} from './i18n'; +import {RhsViewState} from './rhs'; + +import {DraggingState} from '.'; + +export type ViewsState = { + admin: { + navigationBlock: { + blocked: boolean; + onNavigationConfirmed: () => void; + showNavigationPrompt: boolean; + }; + }; + + browser: { + focused: boolean; + }; + + channel: { + postVisibility: { + [channelId: string]: number; + }; + lastChannelViewTime: { + [channelId: string]: number; + }; + loadingPost: { + [channelId: string]: boolean; + }; + focusedPostId: string; + mobileView: boolean; + lastUnreadChannel: (Channel & {hadMentions: boolean}) | null; // Actually only an object with {id: string, hadMentions: boolean} + lastGetPosts: { + [channelId: string]: number; + }; + channelPrefetchStatus: { + [channelId: string]: string; + }; + }; + + rhs: RhsViewState; + + posts: { + editingPost: { + show: boolean; + }; + menuActions: { + [postId: string]: { + [actionId: string]: { + text: string; + value: string; + }; + }; + }; + }; + + modals: { + [modalId: string]: { + open: boolean; + dialogProps: Dictionary; + dialogType: React.Component; + }; + }; + + emoji: { + emojiPickerCustomPage: 0; + }; + + i18n: I18nState; + + lhs: { + isOpen: boolean; + }; + + search: { + modalSearch: string; + modalFilters: { + roles?: string[]; + channel_roles?: string[]; + team_roles?: string[]; + }; + systemUsersSearch: { + term: string; + team: string; + filter: string; + }; + userGridSearch: { + term: string; + filters: { + roles?: string[]; + channel_roles?: string[]; + team_roles?: string[]; + }; + }; + }; + + notice: { + [noticeType: string]: boolean; + }; + + system: { + websocketConnectErrorCount: number; + }; + + channelSelectorModal: { + channels: Channel[]; + }; + + settings: { + activeSection: string; + previousActiveSection: string; + }; + + marketplace: { + plugins: MarketplacePlugin[]; + installing: {[pluginId: string]: boolean}; + errors: {[pluginId: string]: string}; + filter: string; + }; + + channelSidebar: { + unreadFilterEnabled: boolean; + draggingState: DraggingState; + newCategoryIds: string[]; + }; + + nextSteps: { + show: boolean; + }; +}; From 12223b35568ea9079e2c313c99e153b2d463f14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 4 Nov 2020 19:10:24 +0100 Subject: [PATCH 02/11] Use correct react-overlays types version --- package-lock.json | 14 ++++++-------- package.json | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fe0370684f7..252f953be39b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4856,7 +4856,6 @@ "version": "16.9.49", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.49.tgz", "integrity": "sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==", - "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4865,8 +4864,7 @@ "csstype": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", - "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==", - "dev": true + "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==" } } }, @@ -4926,11 +4924,12 @@ } }, "@types/react-overlays": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/react-overlays/-/react-overlays-3.1.0.tgz", - "integrity": "sha512-NzZZHFLj7M7+I+p5rDdVHtm6AeVYQPShVxALiLYhR9leJSX8XujPsYuY+vh7/mzFjv6XR7PxHBAdlFGNaN6QDQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/react-overlays/-/react-overlays-1.1.3.tgz", + "integrity": "sha512-oOq5NWbyfNz2w2sKvjkHdvGQSMA+VDVfI5UOfGPR0wkik2welad1RDVnVgH15jKf58jrZNBa1Ee4SVBgCGFxCg==", "requires": { - "react-overlays": "*" + "@types/react": "*", + "@types/react-transition-group": "*" } }, "@types/react-redux": { @@ -4999,7 +4998,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", - "dev": true, "requires": { "@types/react": "*" } diff --git a/package.json b/package.json index 1fe4a9342588..3dfb4fa336c7 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@stripe/react-stripe-js": "1.1.2", "@stripe/stripe-js": "1.9.0", "@types/country-list": "2.1.0", - "@types/react-overlays": "3.1.0", + "@types/react-overlays": "1.1.3", "bootstrap": "3.4.1", "chart.js": "2.9.3", "compass-mixins": "0.12.10", From ef0244051ab75cb33ee1dffc1929968d24d930ce Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 10 Nov 2020 16:37:24 -0500 Subject: [PATCH 03/11] PR feedback --- client/websocket_client.tsx | 18 +++++++---------- components/dot_menu/dot_menu.tsx | 20 +++++++++++-------- components/dot_menu/index.ts | 8 +++++++- .../channel_header_plug.tsx | 2 +- stores/browser_store.tsx | 2 +- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/client/websocket_client.tsx b/client/websocket_client.tsx index b1ca10d6f000..81c169ae1606 100644 --- a/client/websocket_client.tsx +++ b/client/websocket_client.tsx @@ -190,13 +190,10 @@ export default class WebSocketClient { } userTyping(channelId: string, parentId: string, callback?: () => void) { - const data = {} as { - channel_id: string, - parent_id: string, - }; // TODO is this a defined type? - data.channel_id = channelId; - data.parent_id = parentId; - + const data = { + channel_id: channelId, + parent_id: parentId, + }; this.sendMessage('user_typing', data, callback); } @@ -213,10 +210,9 @@ export default class WebSocketClient { } getStatusesByIds(userIds: string[], callback?: () => void) { - const data = {} as { - user_ids: string[], - }; // TODO is this a defined type? - data.user_ids = userIds; + const data = { + user_ids: userIds, + }; this.sendMessage('get_statuses_by_ids', data, callback); } } diff --git a/components/dot_menu/dot_menu.tsx b/components/dot_menu/dot_menu.tsx index adb075143a2e..614aebe4888f 100644 --- a/components/dot_menu/dot_menu.tsx +++ b/components/dot_menu/dot_menu.tsx @@ -48,7 +48,7 @@ export type Props = { isReadOnly?: boolean; pluginMenuItems: PluginComponent[]; isLicensed: boolean; - postEditTimeLimit?: string; + postEditTimeLimit?: number; enableEmojiPicker: boolean; channelIsArchived: boolean; currentTeamUrl: string; @@ -140,12 +140,12 @@ export default class DotMenu extends React.PureComponent { const {post, isLicensed} = this.props; const {canEdit} = this.state; - const postEditTimeLimit = this.props.postEditTimeLimit || String(Constants.UNSET_POST_EDIT_TIME_LIMIT); + const postEditTimeLimit = this.props.postEditTimeLimit || Constants.UNSET_POST_EDIT_TIME_LIMIT; if (canEdit && isLicensed) { - if (String(postEditTimeLimit) !== String(Constants.UNSET_POST_EDIT_TIME_LIMIT)) { + if (postEditTimeLimit !== Constants.UNSET_POST_EDIT_TIME_LIMIT) { const milliseconds = 1000; - const timeLeft = (post.create_at + (parseInt(postEditTimeLimit, 10) * milliseconds)) - Utils.getTimestamp(); + const timeLeft = (post.create_at + (postEditTimeLimit * milliseconds)) - Utils.getTimestamp(); if (timeLeft > 0) { this.editDisableAction.fireAfter(timeLeft + milliseconds); } @@ -181,7 +181,7 @@ export default class DotMenu extends React.PureComponent { } // listen to clicks/taps on add reaction menu item and pass to parent handler - handleAddReactionMenuItemActivated = (e: Event) => { + handleAddReactionMenuItemActivated = (e: React.MouseEvent) => { e.preventDefault(); // to be safe, make sure the handler function has been defined @@ -202,12 +202,12 @@ export default class DotMenu extends React.PureComponent { } } - handleUnreadMenuItemActivated = (e: Event) => { + handleUnreadMenuItemActivated = (e: React.MouseEvent) => { e.preventDefault(); this.props.actions.markPostAsUnread(this.props.post); } - handleDeleteMenuItemActivated = (e: Event) => { + handleDeleteMenuItemActivated = (e: React.MouseEvent) => { e.preventDefault(); const deletePostModalData = { @@ -250,9 +250,13 @@ export default class DotMenu extends React.PureComponent { ) - refCallback = (menuRef?: any) => { // TODO use more specific type + refCallback = (menuRef?: Menu) => { // TODO use more specific type if (menuRef) { const rect = menuRef.rect(); + if (!rect) { + return; + } + const buttonRect = this.buttonRef.current?.getBoundingClientRect(); const y = (typeof buttonRect?.y === 'undefined' ? buttonRect?.top : buttonRect.y) || 0; const windowHeight = window.innerHeight; diff --git a/components/dot_menu/index.ts b/components/dot_menu/index.ts index 803a715296db..e35a9d84d214 100644 --- a/components/dot_menu/index.ts +++ b/components/dot_menu/index.ts @@ -22,6 +22,7 @@ import { } from 'actions/post_actions.jsx'; import {GlobalState} from 'types/store'; import * as PostUtils from 'utils/post_utils.jsx'; +import {Constants} from 'utils/constants'; import {isArchivedChannel} from 'utils/channel_utils'; import {getSiteURL} from 'utils/url'; @@ -51,10 +52,15 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) { const currentTeam = getCurrentTeam(state) || {}; const currentTeamUrl = `${getSiteURL()}/${currentTeam.name}`; + let postEditTimeLimit; + if (config.PostEditTimeLimit) { + postEditTimeLimit = parseInt(config.PostEditTimeLimit, 10); + } + return { channelIsArchived: isArchivedChannel(channel), components: state.plugins.components, - postEditTimeLimit: getConfig(state).PostEditTimeLimit, + postEditTimeLimit, isLicensed: getLicense(state).IsLicensed === 'true', teamId: getCurrentTeamId(state), pluginMenuItems: state.plugins.components.PostDropdownMenu, diff --git a/plugins/channel_header_plug/channel_header_plug.tsx b/plugins/channel_header_plug/channel_header_plug.tsx index 0a89422ba867..cecf2d6325f1 100644 --- a/plugins/channel_header_plug/channel_header_plug.tsx +++ b/plugins/channel_header_plug/channel_header_plug.tsx @@ -116,7 +116,7 @@ export default class ChannelHeaderPlug extends React.PureComponent void) => { // TODO add more specific types + fireActionAndClose = (action: (channel: Channel, channelMember: ChannelMembership) => void) => { // TODO add more specific types action(this.props.channel, this.props.channelMember); this.onClose(); } diff --git a/stores/browser_store.tsx b/stores/browser_store.tsx index fa015274a491..bb1ea61031b7 100644 --- a/stores/browser_store.tsx +++ b/stores/browser_store.tsx @@ -71,7 +71,7 @@ class BrowserStoreClass { return loginId === sessionStorage.getItem(StoragePrefixes.LOGIN); } - clear(options?: any) { // TODO add more specific types + clear(options?: {exclude?: string[]}) { // TODO add more specific types dispatch(Actions.clear(options)); } From 2399335b4aa9cc4f8a433faf9944aa89890459e7 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 10 Nov 2020 17:14:16 -0500 Subject: [PATCH 04/11] fix types --- components/dot_menu/dot_menu.test.tsx | 2 +- components/dot_menu/dot_menu.tsx | 6 +++--- components/dot_menu/dot_menu_empty.test.tsx | 2 +- components/dot_menu/dot_menu_mobile.test.tsx | 2 +- components/dot_menu/index.ts | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/components/dot_menu/dot_menu.test.tsx b/components/dot_menu/dot_menu.test.tsx index 24b067d00867..26b8d8421c5d 100644 --- a/components/dot_menu/dot_menu.test.tsx +++ b/components/dot_menu/dot_menu.test.tsx @@ -22,7 +22,7 @@ describe('components/dot_menu/DotMenu', () => { const baseProps = { post: {id: 'post_id_1', is_pinned: false} as Post, isLicensed: false, - postEditTimeLimit: '-1', + postEditTimeLimit: -1, handleCommentClick: jest.fn(), handleDropdownOpened: jest.fn(), enableEmojiPicker: true, diff --git a/components/dot_menu/dot_menu.tsx b/components/dot_menu/dot_menu.tsx index fca902a4b50f..228be8d503c1 100644 --- a/components/dot_menu/dot_menu.tsx +++ b/components/dot_menu/dot_menu.tsx @@ -41,11 +41,11 @@ export type Props = { location: Location; commentCount?: number; isFlagged?: boolean; - handleCommentClick?: () => void; - handleDropdownOpened?: () => void; + handleCommentClick?: React.EventHandler; + handleDropdownOpened?: (open: boolean) => void; handleAddReactionClick?: () => void; isMenuOpen?: boolean; - isReadOnly?: boolean; + isReadOnly: boolean | null; pluginMenuItems: PluginComponent[]; isLicensed?: boolean; // TechDebt: Made non-mandatory while converting to typescript postEditTimeLimit?: number; // TechDebt: Made non-mandatory while converting to typescript diff --git a/components/dot_menu/dot_menu_empty.test.tsx b/components/dot_menu/dot_menu_empty.test.tsx index f100b1e566ea..03104e3c671c 100644 --- a/components/dot_menu/dot_menu_empty.test.tsx +++ b/components/dot_menu/dot_menu_empty.test.tsx @@ -29,7 +29,7 @@ describe('components/dot_menu/DotMenu returning empty ("")', () => { const baseProps = { post: {id: 'post_id_1'} as Post, isLicensed: false, - postEditTimeLimit: '-1', + postEditTimeLimit: -1, enableEmojiPicker: true, components: {}, channelIsArchived: false, diff --git a/components/dot_menu/dot_menu_mobile.test.tsx b/components/dot_menu/dot_menu_mobile.test.tsx index 0db4c8f1dc86..219208a2babb 100644 --- a/components/dot_menu/dot_menu_mobile.test.tsx +++ b/components/dot_menu/dot_menu_mobile.test.tsx @@ -29,7 +29,7 @@ describe('components/dot_menu/DotMenu on mobile view', () => { const baseProps = { post: {id: 'post_id_1'} as Post, isLicensed: false, - postEditTimeLimit: '-1', + postEditTimeLimit: -1, enableEmojiPicker: true, components: {}, channelIsArchived: false, diff --git a/components/dot_menu/index.ts b/components/dot_menu/index.ts index e35a9d84d214..589a1d327d68 100644 --- a/components/dot_menu/index.ts +++ b/components/dot_menu/index.ts @@ -33,11 +33,11 @@ type OwnProps = { post: Post; commentCount?: number; isFlagged?: boolean; - handleCommentClick?: () => void; - handleDropdownOpened?: () => void; + handleCommentClick?: React.EventHandler; + handleDropdownOpened?: (open: boolean) => void; handleAddReactionClick?: () => void; isMenuOpen?: boolean; - isReadOnly?: boolean; + isReadOnly: boolean | null; enableEmojiPicker: boolean; location: Location, } From ee9919261066df97e76449562a814dc76c29ad33 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 10 Nov 2020 17:17:24 -0500 Subject: [PATCH 05/11] fix types --- components/dot_menu/dot_menu.test.tsx | 1 + components/dot_menu/dot_menu_empty.test.tsx | 1 + components/dot_menu/dot_menu_mobile.test.tsx | 1 + 3 files changed, 3 insertions(+) diff --git a/components/dot_menu/dot_menu.test.tsx b/components/dot_menu/dot_menu.test.tsx index 26b8d8421c5d..e5ba96957cbe 100644 --- a/components/dot_menu/dot_menu.test.tsx +++ b/components/dot_menu/dot_menu.test.tsx @@ -22,6 +22,7 @@ describe('components/dot_menu/DotMenu', () => { const baseProps = { post: {id: 'post_id_1', is_pinned: false} as Post, isLicensed: false, + isReadOnly: false, postEditTimeLimit: -1, handleCommentClick: jest.fn(), handleDropdownOpened: jest.fn(), diff --git a/components/dot_menu/dot_menu_empty.test.tsx b/components/dot_menu/dot_menu_empty.test.tsx index 03104e3c671c..4c9d402087cd 100644 --- a/components/dot_menu/dot_menu_empty.test.tsx +++ b/components/dot_menu/dot_menu_empty.test.tsx @@ -29,6 +29,7 @@ describe('components/dot_menu/DotMenu returning empty ("")', () => { const baseProps = { post: {id: 'post_id_1'} as Post, isLicensed: false, + isReadOnly: false, postEditTimeLimit: -1, enableEmojiPicker: true, components: {}, diff --git a/components/dot_menu/dot_menu_mobile.test.tsx b/components/dot_menu/dot_menu_mobile.test.tsx index 219208a2babb..55539766673f 100644 --- a/components/dot_menu/dot_menu_mobile.test.tsx +++ b/components/dot_menu/dot_menu_mobile.test.tsx @@ -29,6 +29,7 @@ describe('components/dot_menu/DotMenu on mobile view', () => { const baseProps = { post: {id: 'post_id_1'} as Post, isLicensed: false, + isReadOnly: false, postEditTimeLimit: -1, enableEmojiPicker: true, components: {}, From bf93b8d9bd0fb910aee1ffb7d9dec5aaefa88b79 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 10 Nov 2020 17:18:48 -0500 Subject: [PATCH 06/11] temporarily use any --- stores/browser_store.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/browser_store.tsx b/stores/browser_store.tsx index bb1ea61031b7..207b6b20c744 100644 --- a/stores/browser_store.tsx +++ b/stores/browser_store.tsx @@ -71,7 +71,7 @@ class BrowserStoreClass { return loginId === sessionStorage.getItem(StoragePrefixes.LOGIN); } - clear(options?: {exclude?: string[]}) { // TODO add more specific types + clear(options?: {exclude: any}) { // TODO add more specific types dispatch(Actions.clear(options)); } From b6f52f0a3ff8e235cca7a90b668d03b51992e103 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Tue, 10 Nov 2020 17:19:48 -0500 Subject: [PATCH 07/11] remove unused import --- components/dot_menu/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/components/dot_menu/index.ts b/components/dot_menu/index.ts index 589a1d327d68..b525d2ba5986 100644 --- a/components/dot_menu/index.ts +++ b/components/dot_menu/index.ts @@ -22,7 +22,6 @@ import { } from 'actions/post_actions.jsx'; import {GlobalState} from 'types/store'; import * as PostUtils from 'utils/post_utils.jsx'; -import {Constants} from 'utils/constants'; import {isArchivedChannel} from 'utils/channel_utils'; import {getSiteURL} from 'utils/url'; From 4fc987db1a24d8f8c7f47f8b617a6b05051d9d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Mon, 16 Nov 2020 12:14:20 +0100 Subject: [PATCH 08/11] Remove unneeded comments --- client/websocket_client.tsx | 2 +- plugins/channel_header_plug/channel_header_plug.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/websocket_client.tsx b/client/websocket_client.tsx index 81c169ae1606..f61eebabee1f 100644 --- a/client/websocket_client.tsx +++ b/client/websocket_client.tsx @@ -170,7 +170,7 @@ export default class WebSocketClient { } } - sendMessage(action: string, data: any, responseCallback?: () => void) { // TODO data any or unknown? + sendMessage(action: string, data: any, responseCallback?: () => void) { const msg = { action, seq: this.sequence++, diff --git a/plugins/channel_header_plug/channel_header_plug.tsx b/plugins/channel_header_plug/channel_header_plug.tsx index cecf2d6325f1..ceeb7b52eee8 100644 --- a/plugins/channel_header_plug/channel_header_plug.tsx +++ b/plugins/channel_header_plug/channel_header_plug.tsx @@ -116,7 +116,7 @@ export default class ChannelHeaderPlug extends React.PureComponent void) => { // TODO add more specific types + fireActionAndClose = (action: (channel: Channel, channelMember: ChannelMembership) => void) => { action(this.props.channel, this.props.channelMember); this.onClose(); } From 2d3b89ff4b121776ed75a0cbf86a7357012b4e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Mon, 16 Nov 2020 18:15:56 +0100 Subject: [PATCH 09/11] Extract stringToNumber to utils --- actions/global_actions.tsx | 12 ++---------- utils/utils.jsx | 8 ++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/actions/global_actions.tsx b/actions/global_actions.tsx index aa9e174ba299..9c7374009591 100644 --- a/actions/global_actions.tsx +++ b/actions/global_actions.tsx @@ -229,8 +229,8 @@ export function emitLocalUserTypingEvent(channelId: string, parentPostId: string const stats = getCurrentChannelStats(state); const membersInChannel = stats ? stats.member_count : 0; - const timeBetweenUserTypingUpdatesMilliseconds = stringToNumber(config.TimeBetweenUserTypingUpdatesMilliseconds); - const maxNotificationsPerChannel = stringToNumber(config.MaxNotificationsPerChannel); + const timeBetweenUserTypingUpdatesMilliseconds = Utils.stringToNumber(config.TimeBetweenUserTypingUpdatesMilliseconds); + const maxNotificationsPerChannel = Utils.stringToNumber(config.MaxNotificationsPerChannel); if (((t - lastTimeTypingSent) > timeBetweenUserTypingUpdatesMilliseconds) && (membersInChannel < maxNotificationsPerChannel) && (config.EnableUserTypingMessages === 'true')) { @@ -244,14 +244,6 @@ export function emitLocalUserTypingEvent(channelId: string, parentPostId: string return dispatch(userTyping); } -function stringToNumber(s?: string): number { - if (!s) { - return 0; - } - - return parseInt(s, 10); -} - export function emitUserLoggedOutEvent(redirectTo = '/', shouldSignalLogout = true, userAction = true) { // If the logout was intentional, discard knowledge about having previously been logged in. // This bit is otherwise used to detect session expirations on the login page. diff --git a/utils/utils.jsx b/utils/utils.jsx index 5a1eabb3b2ba..931f9528fafd 100644 --- a/utils/utils.jsx +++ b/utils/utils.jsx @@ -1902,3 +1902,11 @@ export function getNextBillingDate() { const nextBillingDate = moment().add(1, 'months').startOf('month'); return nextBillingDate.format('MMM D, YYYY'); } + +export function stringToNumber(s) { + if (!s) { + return 0; + } + + return parseInt(s, 10); +} From b99600483b66a828659a65e0ce2fd8dd351d5226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Fri, 4 Dec 2020 14:39:17 +0100 Subject: [PATCH 10/11] Fix minor warning on the console --- plugins/channel_header_plug/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/channel_header_plug/index.ts b/plugins/channel_header_plug/index.ts index 27aa07f9b58d..1065ff86d1fc 100644 --- a/plugins/channel_header_plug/index.ts +++ b/plugins/channel_header_plug/index.ts @@ -10,7 +10,7 @@ import ChannelHeaderPlug from './channel_header_plug'; function mapStateToProps(state: GlobalState) { return { - components: state.plugins.components.ChannelHeaderButton, + components: state.plugins.components.ChannelHeaderButton || [], theme: getTheme(state), }; } From f51fe3943a6dcedc66d833e3a63ce63fec104851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Fri, 4 Dec 2020 15:13:52 +0100 Subject: [PATCH 11/11] Fix lint --- components/logged_in/logged_in.test.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/logged_in/logged_in.test.jsx b/components/logged_in/logged_in.test.jsx index c86045224104..c1f2111d1c19 100644 --- a/components/logged_in/logged_in.test.jsx +++ b/components/logged_in/logged_in.test.jsx @@ -6,7 +6,7 @@ import {shallow} from 'enzyme'; import LoggedIn from 'components/logged_in/logged_in.jsx'; import BrowserStore from 'stores/browser_store'; -import * as GlobalActions from 'actions/global_actions.jsx'; +import * as GlobalActions from 'actions/global_actions'; jest.mock('actions/websocket_actions.jsx', () => ({ initialize: jest.fn(),