diff --git a/webapp/channels/package.json b/webapp/channels/package.json index 8426757577..9e8a762af5 100644 --- a/webapp/channels/package.json +++ b/webapp/channels/package.json @@ -140,6 +140,7 @@ "@types/shallow-equals": "1.0.3", "@types/styled-components": "5.1.32", "@types/tinycolor2": "1.4.6", + "@types/zen-observable": "0.8.7", "copy-webpack-plugin": "11.0.0", "emoji-datasource": "6.1.1", "emoji-datasource-apple": "6.1.1", diff --git a/webapp/channels/src/actions/global_actions.test.ts b/webapp/channels/src/actions/global_actions.test.ts index 8dcb1b3327..bd9fa82a84 100644 --- a/webapp/channels/src/actions/global_actions.test.ts +++ b/webapp/channels/src/actions/global_actions.test.ts @@ -13,6 +13,8 @@ import reduxStore from 'stores/redux_store'; import mockStore from 'tests/test_store'; import {getHistory} from 'utils/browser_history'; +const getState = jest.mocked(reduxStore.getState); + jest.mock('actions/views/rhs', () => ({ closeMenu: jest.fn(), closeRightHandSide: jest.fn(), @@ -64,7 +66,7 @@ describe('actions/global_actions', () => { }, }); - reduxStore.getState.mockImplementation(store.getState); + getState.mockImplementation(store.getState); await redirectUserToDefaultTeam(); expect(getHistory().push).toHaveBeenCalledWith('/select_team'); @@ -138,7 +140,7 @@ describe('actions/global_actions', () => { }, }); - reduxStore.getState.mockImplementation(store.getState); + getState.mockImplementation(store.getState); await redirectUserToDefaultTeam(); expect(getHistory().push).toHaveBeenCalledWith('/team2/channels/channel-in-team-2'); @@ -211,7 +213,7 @@ describe('actions/global_actions', () => { }, }); - reduxStore.getState.mockImplementation(store.getState); + getState.mockImplementation(store.getState); await redirectUserToDefaultTeam(); expect(getHistory().push).toHaveBeenCalledWith('/team2/channels/channel-in-team-2'); @@ -283,7 +285,7 @@ describe('actions/global_actions', () => { }, }); - reduxStore.getState.mockImplementation(store.getState); + getState.mockImplementation(store.getState); await redirectUserToDefaultTeam(); expect(getHistory().push).toHaveBeenCalledWith('/select_team'); @@ -315,7 +317,7 @@ describe('actions/global_actions', () => { }, }); - reduxStore.getState.mockImplementation(store.getState); + getState.mockImplementation(store.getState); await redirectUserToDefaultTeam(); expect(getHistory().push).not.toHaveBeenCalled(); @@ -408,7 +410,7 @@ describe('actions/global_actions', () => { }, }, }); - reduxStore.getState.mockImplementation(store.getState); + getState.mockImplementation(store.getState); LocalStorageStore.setPreviousTeamId(userId, teamId); LocalStorageStore.setPreviousChannelName(userId, teamId, directChannelId); @@ -505,7 +507,7 @@ describe('actions/global_actions', () => { }, }, }); - reduxStore.getState.mockImplementation(store.getState); + getState.mockImplementation(store.getState); LocalStorageStore.setPreviousTeamId(userId, teamId); LocalStorageStore.setPreviousChannelName(userId, teamId, groupChannelId); @@ -567,7 +569,7 @@ describe('actions/global_actions', () => { }, }); - reduxStore.getState.mockImplementation(store.getState); + getState.mockImplementation(store.getState); await redirectUserToDefaultTeam(); expect(getHistory().push).toHaveBeenCalledWith('/team1/channels/channel-in-team-1'); diff --git a/webapp/channels/src/actions/user_actions.test.ts b/webapp/channels/src/actions/user_actions.test.ts index 8f62fd1238..22aa9a940e 100644 --- a/webapp/channels/src/actions/user_actions.test.ts +++ b/webapp/channels/src/actions/user_actions.test.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type {Dispatch, AnyAction} from 'redux'; + import type {Channel, ChannelMembership, ChannelMessageCount} from '@mattermost/types/channels'; import type {Post} from '@mattermost/types/posts'; import type {Team, TeamMembership} from '@mattermost/types/teams'; @@ -601,8 +603,8 @@ describe('Actions.User', () => { } as unknown as GlobalState; const testStore = mockStore(state); - store.getState.mockImplementation(testStore.getState); - store.dispatch.mockImplementation(testStore.dispatch); + (store.getState as jest.MockedFunction<() => GlobalState>).mockImplementation(testStore.getState); + (store.dispatch as jest.MockedFunction>).mockImplementation(testStore.dispatch); const actions = testStore.getActions(); await UserActions.loadProfilesForGM(); diff --git a/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.test.tsx b/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.test.tsx index 2387b14bc7..6e528e7b2e 100644 --- a/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.test.tsx +++ b/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.test.tsx @@ -8,7 +8,7 @@ import {TestHelper} from 'utils/test_helper'; import SearchChannelWithPermissionsProvider from './search_channel_with_permissions_provider'; -const getState = store.getState; +const getState = jest.mocked(store.getState); jest.mock('stores/redux_store', () => ({ dispatch: jest.fn(), diff --git a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts index 42563cc7e5..dccc2edcfd 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts @@ -133,6 +133,7 @@ const state: GlobalState = { commands: {}, appsBotIDs: [], appsOAuthAppIDs: [], + dialogTriggerId: '', outgoingOAuthConnections: {}, }, files: { diff --git a/webapp/channels/src/plugins/index.ts b/webapp/channels/src/plugins/index.ts index 0a9526db40..3e08596407 100644 --- a/webapp/channels/src/plugins/index.ts +++ b/webapp/channels/src/plugins/index.ts @@ -3,7 +3,7 @@ import regeneratorRuntime from 'regenerator-runtime'; -import type {PluginManifest} from '@mattermost/types/plugins'; +import type {PluginManifest, ClientPluginManifest} from '@mattermost/types/plugins'; import {Client4} from 'mattermost-redux/client'; import {Preferences} from 'mattermost-redux/constants'; @@ -102,8 +102,8 @@ export async function initializePlugins(): Promise { return; } - await Promise.all(data.map((m: PluginManifest) => { - return loadPlugin(m).catch((loadErr: Error) => { + await Promise.all(data.map(async (m: ClientPluginManifest) => { + return loadPlugin(m as PluginManifest).catch((loadErr: Error) => { console.error(loadErr.message); //eslint-disable-line no-console }); })); @@ -112,7 +112,7 @@ export async function initializePlugins(): Promise { } // getPlugins queries the server for all enabled plugins -export function getPlugins(): ActionFuncAsync { +export function getPlugins(): ActionFuncAsync { return async (dispatch) => { let plugins; try { diff --git a/webapp/channels/src/plugins/interactive_dialog.ts b/webapp/channels/src/plugins/interactive_dialog.ts index 1107644e5f..56acf615e1 100644 --- a/webapp/channels/src/plugins/interactive_dialog.ts +++ b/webapp/channels/src/plugins/interactive_dialog.ts @@ -34,8 +34,8 @@ store.subscribe(() => { previousTriggerId = currentTriggerId; - const dialog = state.entities.integrations.dialog || {}; - if (dialog.trigger_id !== currentTriggerId) { + const dialog = state.entities.integrations.dialog; + if (!dialog || dialog.trigger_id !== currentTriggerId) { return; } diff --git a/webapp/channels/src/selectors/local_storage.ts b/webapp/channels/src/selectors/local_storage.ts index ed9c4b3ed9..383b25c115 100644 --- a/webapp/channels/src/selectors/local_storage.ts +++ b/webapp/channels/src/selectors/local_storage.ts @@ -1,13 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {GlobalState} from '@mattermost/types/store'; - import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import localStorageStore from 'stores/local_storage_store'; +import type {GlobalState} from 'types/store'; + // getLastViewedChannelName combines data from the Redux store and localStorage to return the // previously selected channel name, returning the default channel if none exists. // diff --git a/webapp/channels/src/store/index.test.js b/webapp/channels/src/store/index.test.ts similarity index 100% rename from webapp/channels/src/store/index.test.js rename to webapp/channels/src/store/index.test.ts diff --git a/webapp/channels/src/store/index.js b/webapp/channels/src/store/index.ts similarity index 81% rename from webapp/channels/src/store/index.js rename to webapp/channels/src/store/index.ts index a223adc0ae..21d79958a6 100644 --- a/webapp/channels/src/store/index.js +++ b/webapp/channels/src/store/index.ts @@ -3,9 +3,13 @@ import baseLocalForage from 'localforage'; import {extendPrototype} from 'localforage-observable'; +import type {Store} from 'redux'; +import type {Persistor} from 'redux-persist'; import {persistStore, REHYDRATE} from 'redux-persist'; import Observable from 'zen-observable'; +import type {DeepPartial} from '@mattermost/types/utilities'; + import {General, RequestStatus} from 'mattermost-redux/constants'; import configureServiceStore from 'mattermost-redux/store'; @@ -14,15 +18,23 @@ import {clearUserCookie} from 'actions/views/cookie'; import appReducers from 'reducers'; import {getBasePath} from 'selectors/general'; +import type {GlobalState} from 'types/store'; + function getAppReducers() { return require('../reducers'); // eslint-disable-line global-require } +declare global { + interface Window { + Observable: typeof Observable; + } +} + window.Observable = Observable; const localForage = extendPrototype(baseLocalForage); -export default function configureStore(preloadedState, additionalReducers) { +export default function configureStore(preloadedState?: DeepPartial, additionalReducers?: Record): Store { const reducers = additionalReducers ? {...appReducers, ...additionalReducers} : appReducers; const store = configureServiceStore({ appReducers: reducers, @@ -31,7 +43,7 @@ export default function configureStore(preloadedState, additionalReducers) { }); localForage.ready().then(() => { - const persistor = persistStore(store, null, () => { + const persistor: Persistor = persistStore(store, null, () => { store.dispatch({ type: General.STORE_REHYDRATION_COMPLETE, complete: true, @@ -51,23 +63,23 @@ export default function configureStore(preloadedState, additionalReducers) { // Rehydrate redux-persist when another tab changes localForage observable.subscribe({ - next: (args) => { - if (!args.crossTabNotification) { + next: (value) => { + if (!value.crossTabNotification) { // Ignore changes made by this tab return; } const keyPrefix = 'persist:'; - if (!args.key.startsWith(keyPrefix)) { + if (!value.key.startsWith(keyPrefix)) { // Ignore changes that weren't made by redux-persist return; } - const key = args.key.substring(keyPrefix.length); - const newValue = JSON.parse(args.newValue); + const key = value.key.substring(keyPrefix.length); + const newValue = JSON.parse(value.newValue); - const payload = {}; + const payload: Record = {}; for (const reducerKey of Object.keys(newValue)) { if (reducerKey === '_persist') { @@ -110,7 +122,7 @@ export default function configureStore(preloadedState, additionalReducers) { }); } }); - }).catch((e) => { + }).catch((e: Error) => { // eslint-disable-next-line no-console console.error('Failed to initialize localForage', e); }); @@ -121,11 +133,11 @@ export default function configureStore(preloadedState, additionalReducers) { /** * Migrates state.storage from redux-persist@4 to redux-persist@6 */ -function migratePersistedState(store, persistor) { +function migratePersistedState(store: Store, persistor: Persistor): void { const oldKeyPrefix = 'reduxPersist:storage:'; - const restoredState = {}; - localForage.iterate((value, key) => { + const restoredState: Record = {}; + localForage.iterate((value: string, key: string) => { if (key && key.startsWith(oldKeyPrefix)) { restoredState[key.substring(oldKeyPrefix.length)] = value; } @@ -140,7 +152,7 @@ function migratePersistedState(store, persistor) { persistor.pause(); - const persistedState = {}; + const persistedState: Record = {}; for (const [key, value] of Object.entries(restoredState)) { // eslint-disable-next-line no-console diff --git a/webapp/channels/src/stores/local_storage_store.ts b/webapp/channels/src/stores/local_storage_store.ts index ee25ecca37..8528a38248 100644 --- a/webapp/channels/src/stores/local_storage_store.ts +++ b/webapp/channels/src/stores/local_storage_store.ts @@ -91,12 +91,12 @@ class LocalStorageStoreClass { this.removeItem(getPenultimateChannelNameKey(userId, teamId)); } - removePreviousChannelType(userId: string, teamId: string, state = store.getStore()) { + removePreviousChannelType(userId: string, teamId: string, state = store.getState()) { this.setItem(getPreviousViewedTypeKey(userId, teamId), this.getPenultimateViewedType(userId, teamId, state)); this.removeItem(getPenultimateViewedTypeKey(userId, teamId)); } - removePreviousChannel(userId: string, teamId: string, state = store.getStore()) { + removePreviousChannel(userId: string, teamId: string, state = store.getState()) { this.removePreviousChannelName(userId, teamId, state); this.removePreviousChannelType(userId, teamId, state); } diff --git a/webapp/channels/src/tests/react_testing_utils.tsx b/webapp/channels/src/tests/react_testing_utils.tsx index 8693222b55..dfb3334917 100644 --- a/webapp/channels/src/tests/react_testing_utils.tsx +++ b/webapp/channels/src/tests/react_testing_utils.tsx @@ -145,7 +145,7 @@ function replaceGlobalStore(getStore: () => any) { jest.spyOn(globalStore, 'dispatch').mockImplementation((...args) => getStore().dispatch(...args)); jest.spyOn(globalStore, 'getState').mockImplementation(() => getStore().getState()); jest.spyOn(globalStore, 'replaceReducer').mockImplementation((...args) => getStore().replaceReducer(...args)); - jest.spyOn(globalStore, '@@observable').mockImplementation((...args) => getStore()['@@observable'](...args)); + jest.spyOn(globalStore, '@@observable' as any).mockImplementation((...args: any[]) => getStore()['@@observable'](...args)); // This may stop working if getStore starts to return new results jest.spyOn(globalStore, 'subscribe').mockImplementation((...args) => getStore().subscribe(...args)); diff --git a/webapp/channels/src/utils/utils.test.tsx b/webapp/channels/src/utils/utils.test.tsx index e26f15b002..0d126c664c 100644 --- a/webapp/channels/src/utils/utils.test.tsx +++ b/webapp/channels/src/utils/utils.test.tsx @@ -172,7 +172,7 @@ describe('Utils.localizeMessage', () => { }, }, }, - }); + } as any); }); test('with translations', () => { @@ -200,7 +200,7 @@ describe('Utils.localizeMessage', () => { translations: {}, }, }, - }); + } as any); }); test('without translations', () => { diff --git a/webapp/channels/src/utils/utils.tsx b/webapp/channels/src/utils/utils.tsx index 0d3487c13d..62beab83bd 100644 --- a/webapp/channels/src/utils/utils.tsx +++ b/webapp/channels/src/utils/utils.tsx @@ -1286,7 +1286,7 @@ export async function handleFormattedTextClick(e: React.MouseEvent, currentRelat let post = getPost(state, postId!); if (!post) { const {data: postData} = await store.dispatch(getPostAction(match.postId!)); - post = postData; + post = postData!; } if (post) { isReply = Boolean(post.root_id); @@ -1303,12 +1303,12 @@ export async function handleFormattedTextClick(e: React.MouseEvent, currentRelat if (!member) { const membership = await store.dispatch(getChannelMember(channel.id, getCurrentUserId(state))); if ('data' in membership) { - member = membership.data; + member = membership.data!; } } if (!member) { const {data} = await store.dispatch(joinPrivateChannelPrompt(team, channel.display_name, false)); - if (data.join) { + if (data!.join) { let error = false; if (!getTeamMemberships(state)[team.id]) { const joinTeamResult = await store.dispatch(addUserToTeam(team.id, user.id)); diff --git a/webapp/package-lock.json b/webapp/package-lock.json index b8f66011b2..5bb720e340 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -193,6 +193,7 @@ "@types/shallow-equals": "1.0.3", "@types/styled-components": "5.1.32", "@types/tinycolor2": "1.4.6", + "@types/zen-observable": "0.8.7", "copy-webpack-plugin": "11.0.0", "emoji-datasource": "6.1.1", "emoji-datasource-apple": "6.1.1", @@ -7310,6 +7311,12 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, + "node_modules/@types/zen-observable": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.7.tgz", + "integrity": "sha512-LKzNTjj+2j09wAo/vvVjzgw5qckJJzhdGgWHW7j69QIGdq/KnZrMAMIHQiWGl3Ccflh5/CudBAntTPYdprPltA==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", diff --git a/webapp/platform/types/src/integrations.ts b/webapp/platform/types/src/integrations.ts index 582db3096f..44141b8263 100644 --- a/webapp/platform/types/src/integrations.ts +++ b/webapp/platform/types/src/integrations.ts @@ -134,9 +134,11 @@ export type IntegrationsState = { appsBotIDs: string[]; systemCommands: IDMappedObjects; commands: IDMappedObjects; + dialogTriggerId: string; dialog?: { url: string; dialog: Dialog; + trigger_id: string; }; };