Migrating other 2 files from javascript to typescript (#29435)

* feat: Add initial store configuration for webapp channels

* refactor: Convert store/index.js to TypeScript with type definitions

* test: Add initial test file for store index

* refactor: Convert index.test.js to TypeScript with type annotations

* Removing old files

* Applying linter fixes

* Fixing some of the types errors

* fix: Type mock implementation of getState in global_actions.test.ts

* test: Add missing GlobalState import in global_actions.test.ts

* fix: Resolve TypeScript mock implementation error in global_actions.test.ts

* Some fixes

* Address CI problems

* Installing zen-observable types

* Addressing PR review comment

* Addressing PR review comment

* Addressing PR review comment

* Addressing PR review comment

* Addressing PR review comment

* Simpliying things

* Fixing CI

* Fixing types
This commit is contained in:
Jesús Espino 2025-02-10 15:43:09 +01:00 committed by GitHub
parent 90814564ff
commit 9b87970c99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 67 additions and 40 deletions

View File

@ -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",

View File

@ -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');

View File

@ -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<Dispatch<AnyAction>>).mockImplementation(testStore.dispatch);
const actions = testStore.getActions();
await UserActions.loadProfilesForGM();

View File

@ -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(),

View File

@ -133,6 +133,7 @@ const state: GlobalState = {
commands: {},
appsBotIDs: [],
appsOAuthAppIDs: [],
dialogTriggerId: '',
outgoingOAuthConnections: {},
},
files: {

View File

@ -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<void> {
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<void> {
}
// getPlugins queries the server for all enabled plugins
export function getPlugins(): ActionFuncAsync {
export function getPlugins(): ActionFuncAsync<ClientPluginManifest[]> {
return async (dispatch) => {
let plugins;
try {

View File

@ -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;
}

View File

@ -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.
//

View File

@ -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<GlobalState>, additionalReducers?: Record<string, any>): Store<GlobalState> {
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<string, any> = {};
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<GlobalState>, persistor: Persistor): void {
const oldKeyPrefix = 'reduxPersist:storage:';
const restoredState = {};
localForage.iterate((value, key) => {
const restoredState: Record<string, string> = {};
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<string, any> = {};
for (const [key, value] of Object.entries(restoredState)) {
// eslint-disable-next-line no-console

View File

@ -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);
}

View File

@ -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));

View File

@ -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', () => {

View File

@ -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));

View File

@ -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",

View File

@ -134,9 +134,11 @@ export type IntegrationsState = {
appsBotIDs: string[];
systemCommands: IDMappedObjects<Command>;
commands: IDMappedObjects<Command>;
dialogTriggerId: string;
dialog?: {
url: string;
dialog: Dialog;
trigger_id: string;
};
};