Improve parallelization of loadMeAndConfig action (#27858)
1. Rewrite of loadConfigAndMe function which includes compatible return types along with less blocking redux actions 2. Actions of webapp/channels/src/components/root/actions.ts moved to webapp/channels/src/actions/views/root.ts for consistency 3. Removes 2 redundant network requests for config and license.
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
daff7a39c5
Коммит
15c9b15f67
@@ -7,23 +7,6 @@ import * as i18nSelectors from 'selectors/i18n';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {ActionTypes} from 'utils/constants';
|
||||
|
||||
jest.mock('mattermost-redux/actions/general', () => {
|
||||
const original = jest.requireActual('mattermost-redux/actions/general');
|
||||
return {
|
||||
...original,
|
||||
getClientConfig: () => ({type: 'MOCK_GET_CLIENT_CONFIG'}),
|
||||
getLicenseConfig: () => ({type: 'MOCK_GET_LICENSE_CONFIG'}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('mattermost-redux/actions/users', () => {
|
||||
const original = jest.requireActual('mattermost-redux/actions/users');
|
||||
return {
|
||||
...original,
|
||||
loadMe: () => ({type: 'MOCK_LOAD_ME'}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('root view actions', () => {
|
||||
const origCookies = document.cookie;
|
||||
const origWasLoggedIn = localStorage.getItem('was_logged_in');
|
||||
@@ -38,25 +21,6 @@ describe('root view actions', () => {
|
||||
localStorage.setItem('was_logged_in', origWasLoggedIn || '');
|
||||
});
|
||||
|
||||
describe('loadConfigAndMe', () => {
|
||||
test('loadConfigAndMe, without user logged in', async () => {
|
||||
const testStore = mockStore({});
|
||||
|
||||
await testStore.dispatch(Actions.loadConfigAndMe());
|
||||
expect(testStore.getActions()).toEqual([{type: 'MOCK_GET_CLIENT_CONFIG'}, {type: 'MOCK_GET_LICENSE_CONFIG'}]);
|
||||
});
|
||||
|
||||
test('loadConfigAndMe, with user logged in', async () => {
|
||||
const testStore = mockStore({});
|
||||
|
||||
document.cookie = 'MMUSERID=userid';
|
||||
localStorage.setItem('was_logged_in', 'true');
|
||||
|
||||
await testStore.dispatch(Actions.loadConfigAndMe());
|
||||
expect(testStore.getActions()).toEqual([{type: 'MOCK_GET_CLIENT_CONFIG'}, {type: 'MOCK_GET_LICENSE_CONFIG'}, {type: 'MOCK_LOAD_ME'}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerPluginTranslationsSource', () => {
|
||||
test('Should not dispatch action when getTranslation is empty', () => {
|
||||
const testStore = mockStore({});
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ClientConfig} from '@mattermost/types/config';
|
||||
|
||||
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
|
||||
import {loadMe} from 'mattermost-redux/actions/users';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import type {ActionFuncAsync, ThunkActionFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
@@ -20,26 +16,6 @@ const pluginTranslationSources: Record<string, TranslationPluginFunction> = {};
|
||||
|
||||
export type TranslationPluginFunction = (locale: string) => Translations
|
||||
|
||||
export function loadConfigAndMe(): ThunkActionFunc<Promise<{config?: ClientConfig; isMeLoaded: boolean}>> {
|
||||
return async (dispatch) => {
|
||||
const results = await Promise.all([
|
||||
dispatch(getClientConfig()),
|
||||
dispatch(getLicenseConfig()),
|
||||
]);
|
||||
|
||||
let isMeLoaded = false;
|
||||
if (document.cookie.includes('MMUSERID=')) {
|
||||
const dataFromLoadMe = await dispatch(loadMe());
|
||||
isMeLoaded = dataFromLoadMe?.data ?? false;
|
||||
}
|
||||
|
||||
return {
|
||||
config: results[0].data,
|
||||
isMeLoaded,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function registerPluginTranslationsSource(pluginId: string, sourceFunction: TranslationPluginFunction): ThunkActionFunc<void, GlobalState> {
|
||||
pluginTranslationSources[pluginId] = sourceFunction;
|
||||
return (dispatch, getState) => {
|
||||
@@ -91,19 +67,3 @@ export function loadTranslations(locale: string, url: string): ActionFuncAsync {
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function registerCustomPostRenderer(type: string, component: any, id: string): ActionFuncAsync {
|
||||
return async (dispatch) => {
|
||||
// piggyback on plugins state to register a custom post renderer
|
||||
dispatch({
|
||||
type: ActionTypes.RECEIVED_PLUGIN_POST_COMPONENT,
|
||||
data: {
|
||||
postTypeId: id,
|
||||
pluginId: id,
|
||||
type,
|
||||
component,
|
||||
},
|
||||
});
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ import Constants from 'utils/constants';
|
||||
import DesktopApp from 'utils/desktop_api';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
import {getBrowserTimezone} from 'utils/timezone';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import {isAndroid, isIos} from 'utils/user_agent';
|
||||
import {doesCookieContainsMMUserId} from 'utils/utils';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -89,9 +90,9 @@ export default class LoggedIn extends React.PureComponent<Props> {
|
||||
};
|
||||
|
||||
// Device tracking setup
|
||||
if (UserAgent.isIos()) {
|
||||
if (isIos()) {
|
||||
document.body.classList.add('ios');
|
||||
} else if (UserAgent.isAndroid()) {
|
||||
} else if (isAndroid()) {
|
||||
document.body.classList.add('android');
|
||||
}
|
||||
|
||||
@@ -202,7 +203,7 @@ export default class LoggedIn extends React.PureComponent<Props> {
|
||||
private handleBeforeUnload = (): void => {
|
||||
// remove the event listener to prevent getting stuck in a loop
|
||||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||||
if (document.cookie.indexOf('MMUSERID=') > -1 && this.props.currentChannelId && !this.props.isCurrentChannelManuallyUnread) {
|
||||
if (doesCookieContainsMMUserId() && this.props.currentChannelId && !this.props.isCurrentChannelManuallyUnread) {
|
||||
this.props.actions.updateApproximateViewTime(this.props.currentChannelId);
|
||||
}
|
||||
WebSocketActions.close();
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/Root Routes Should mount public product routes 1`] = `
|
||||
<RootProvider>
|
||||
<Connect(MobileViewWatcher) />
|
||||
<LuxonController />
|
||||
<PerformanceReporterController />
|
||||
<Switch>
|
||||
<Route
|
||||
component={[Function]}
|
||||
path="/error"
|
||||
/>
|
||||
<HFRoute
|
||||
component={[Function]}
|
||||
path="/login"
|
||||
/>
|
||||
<HFRoute
|
||||
component={[Function]}
|
||||
path="/access_problem"
|
||||
/>
|
||||
<HFTRoute
|
||||
component={[Function]}
|
||||
path="/reset_password"
|
||||
/>
|
||||
<HFTRoute
|
||||
component={[Function]}
|
||||
path="/reset_password_complete"
|
||||
/>
|
||||
<HFRoute
|
||||
component={[Function]}
|
||||
path="/signup_user_complete"
|
||||
/>
|
||||
<HFRoute
|
||||
component={[Function]}
|
||||
path="/should_verify_email"
|
||||
/>
|
||||
<HFRoute
|
||||
component={[Function]}
|
||||
path="/do_verify_email"
|
||||
/>
|
||||
<HFTRoute
|
||||
component={[Function]}
|
||||
path="/claim"
|
||||
/>
|
||||
<LoggedInRoute
|
||||
component={[Function]}
|
||||
path="/terms_of_service"
|
||||
/>
|
||||
<Route
|
||||
component={[Function]}
|
||||
path="/landing"
|
||||
/>
|
||||
<Route
|
||||
path="/admin_console"
|
||||
>
|
||||
<Switch>
|
||||
<LoggedInRoute
|
||||
component={[Function]}
|
||||
path="/admin_console"
|
||||
theme={Object {}}
|
||||
/>
|
||||
<Connect(RootRedirect) />
|
||||
</Switch>
|
||||
</Route>
|
||||
<LoggedInHFTRoute
|
||||
component={[Function]}
|
||||
path="/select_team"
|
||||
/>
|
||||
<LoggedInHFTRoute
|
||||
component={[Function]}
|
||||
path="/oauth/authorize"
|
||||
/>
|
||||
<LoggedInHFTRoute
|
||||
component={[Function]}
|
||||
path="/create_team"
|
||||
/>
|
||||
<LoggedInRoute
|
||||
component={[Function]}
|
||||
path="/mfa"
|
||||
/>
|
||||
<LoggedInRoute
|
||||
component={[Function]}
|
||||
path="/preparing-workspace"
|
||||
/>
|
||||
<Redirect
|
||||
from="/_redirect/integrations/:subpath*"
|
||||
to="/myTeam/integrations/:subpath*"
|
||||
/>
|
||||
<Redirect
|
||||
from="/_redirect/pl/:postid"
|
||||
to="/myTeam/pl/:postid"
|
||||
/>
|
||||
<CompassThemeProvider
|
||||
theme={Object {}}
|
||||
>
|
||||
<WindowSizeObserver />
|
||||
<Connect(ModalController) />
|
||||
<Connect(Component) />
|
||||
<Connect(injectIntl(SystemNotice)) />
|
||||
<GlobalHeader />
|
||||
<CloudEffectsWrapper />
|
||||
<withRouter(Connect(TeamSidebar)) />
|
||||
<div
|
||||
className="main-wrapper"
|
||||
>
|
||||
<Switch>
|
||||
<Route
|
||||
key="productwithpublic-public"
|
||||
path="/productwithpublic/public"
|
||||
render={[Function]}
|
||||
/>
|
||||
<Route
|
||||
key="productwithpublic"
|
||||
path="/productwithpublic"
|
||||
render={[Function]}
|
||||
/>
|
||||
<Route
|
||||
key="productwithoutpublic"
|
||||
path="/productwithoutpublic"
|
||||
render={[Function]}
|
||||
/>
|
||||
<LoggedInRoute
|
||||
component={[Function]}
|
||||
path="/:team([a-z0-9\\\\-_]+)"
|
||||
theme={Object {}}
|
||||
/>
|
||||
<Connect(RootRedirect) />
|
||||
</Switch>
|
||||
<withRouter(Connect(SidebarRight)) />
|
||||
</div>
|
||||
<Connect(Pluggable)
|
||||
pluggableName="Global"
|
||||
/>
|
||||
<AppBar />
|
||||
<Connect(Component) />
|
||||
</CompassThemeProvider>
|
||||
</Switch>
|
||||
</RootProvider>
|
||||
`;
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {History} from 'history';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {getFirstAdminSetupComplete} from 'mattermost-redux/actions/general';
|
||||
import {getProfiles} from 'mattermost-redux/actions/users';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {checkIsFirstAdmin, getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import type {ThunkActionFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions';
|
||||
|
||||
import {StoragePrefixes} from 'utils/constants';
|
||||
|
||||
export function redirectToOnboardingOrDefaultTeam(history: History): ThunkActionFunc<void> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const isUserAdmin = isCurrentUserSystemAdmin(state);
|
||||
if (!isUserAdmin) {
|
||||
GlobalActions.redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
|
||||
const teams = getActiveTeamsList(state);
|
||||
|
||||
const onboardingFlowEnabled = getIsOnboardingFlowEnabled(state);
|
||||
|
||||
if (teams.length > 0 || !onboardingFlowEnabled) {
|
||||
GlobalActions.redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
|
||||
const firstAdminSetupComplete = await dispatch(getFirstAdminSetupComplete());
|
||||
if (firstAdminSetupComplete?.data) {
|
||||
GlobalActions.redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
|
||||
const profilesResult = await dispatch(getProfiles(0, General.PROFILE_CHUNK_SIZE, {roles: General.SYSTEM_ADMIN_ROLE}));
|
||||
if (profilesResult.error) {
|
||||
GlobalActions.redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
const currentUser = getCurrentUser(getState());
|
||||
const adminProfiles = profilesResult.data?.reduce(
|
||||
(acc: Record<string, UserProfile>, curr: UserProfile) => {
|
||||
acc[curr.id] = curr;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
if (adminProfiles && checkIsFirstAdmin(currentUser, adminProfiles)) {
|
||||
history.push('/preparing-workspace');
|
||||
return;
|
||||
}
|
||||
|
||||
GlobalActions.redirectUserToDefaultTeam();
|
||||
};
|
||||
}
|
||||
|
||||
export function handleLoginLogoutSignal(e: StorageEvent): ThunkActionFunc<void> {
|
||||
return (dispatch, getState) => {
|
||||
// when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
|
||||
const isNewLocalStorageEvent = (event: StorageEvent) => event.storageArea === localStorage && event.newValue;
|
||||
|
||||
if (e.key === StoragePrefixes.LOGOUT && isNewLocalStorageEvent(e)) {
|
||||
console.log('detected logout from a different tab'); //eslint-disable-line no-console
|
||||
GlobalActions.emitUserLoggedOutEvent('/', false, false);
|
||||
}
|
||||
if (e.key === StoragePrefixes.LOGIN && isNewLocalStorageEvent(e)) {
|
||||
const isLoggedIn = getCurrentUser(getState());
|
||||
|
||||
// make sure this is not the same tab which sent login signal
|
||||
// because another tabs will also send login signal after reloading
|
||||
if (isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// detected login from a different tab
|
||||
function reloadOnFocus() {
|
||||
location.reload();
|
||||
}
|
||||
window.addEventListener('focus', reloadOnFocus);
|
||||
}
|
||||
};
|
||||
}
|
||||
92
webapp/channels/src/components/root/actions/index.test.ts
Обычный файл
92
webapp/channels/src/components/root/actions/index.test.ts
Обычный файл
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import {loadConfigAndMe} from './index';
|
||||
|
||||
jest.mock('mattermost-redux/actions/general', () => {
|
||||
const original = jest.requireActual('mattermost-redux/actions/general');
|
||||
return {
|
||||
...original,
|
||||
getClientConfig: () => ({type: 'MOCK_GET_CLIENT_CONFIG'}),
|
||||
getLicenseConfig: () => ({type: 'MOCK_GET_LICENSE_CONFIG'}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('mattermost-redux/actions/users', () => {
|
||||
const original = jest.requireActual('mattermost-redux/actions/users');
|
||||
return {
|
||||
...original,
|
||||
getMe: () => ({type: 'MOCK_LOAD_ME'}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('mattermost-redux/actions/preferences', () => {
|
||||
const original = jest.requireActual('mattermost-redux/actions/preferences');
|
||||
return {
|
||||
...original,
|
||||
getMyPreferences: () => ({type: 'MOCK_LOAD_PREFERENCES'}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('mattermost-redux/actions/teams', () => {
|
||||
const original = jest.requireActual('mattermost-redux/actions/teams');
|
||||
return {
|
||||
...original,
|
||||
getMyTeamMembers: () => ({type: 'MOCK_GET_MY_TEAM_MEMBERS'}),
|
||||
getMyTeams: () => ({type: 'MOCK_GET_MY_TEAMS'}),
|
||||
getMyTeamUnreads: () => ({type: 'MOCK_GET_MY_TEAM_UNREADS'}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/preferences', () => {
|
||||
const original = jest.requireActual('mattermost-redux/selectors/entities/preferences');
|
||||
return {
|
||||
...original,
|
||||
isCollapsedThreadsEnabled: () => false,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('mattermost-redux/actions/limits', () => ({
|
||||
...jest.requireActual('mattermost-redux/actions/limits'),
|
||||
getServerLimits: () => ({type: 'MOCK_GET_SERVER_LIMITS'}),
|
||||
}));
|
||||
|
||||
describe('loadConfigAndMe', () => {
|
||||
test('loadConfigAndMe, without user logged in', async () => {
|
||||
const testStore = mockStore({});
|
||||
|
||||
await testStore.dispatch(loadConfigAndMe());
|
||||
expect(testStore.getActions()).toEqual([{type: 'MOCK_GET_CLIENT_CONFIG'}, {type: 'MOCK_GET_LICENSE_CONFIG'}]);
|
||||
});
|
||||
|
||||
test('loadConfigAndMe, with user logged in', async () => {
|
||||
const testStore = mockStore({
|
||||
entities: {
|
||||
general: {
|
||||
serverVersion: '1.0.0',
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'userid',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
document.cookie = 'MMUSERID=userid';
|
||||
localStorage.setItem('was_logged_in', 'true');
|
||||
|
||||
await testStore.dispatch(loadConfigAndMe());
|
||||
expect(testStore.getActions()).toEqual([
|
||||
{type: 'MOCK_GET_CLIENT_CONFIG'},
|
||||
{type: 'MOCK_GET_LICENSE_CONFIG'},
|
||||
{type: 'RECEIVED_SERVER_VERSION', data: '1.0.0'},
|
||||
{type: 'MOCK_LOAD_ME'},
|
||||
{type: 'MOCK_LOAD_PREFERENCES'},
|
||||
{type: 'MOCK_GET_MY_TEAMS'},
|
||||
{type: 'MOCK_GET_MY_TEAM_MEMBERS'},
|
||||
{type: 'MOCK_GET_MY_TEAM_UNREADS'},
|
||||
{type: 'MOCK_GET_SERVER_LIMITS'},
|
||||
]);
|
||||
});
|
||||
});
|
||||
173
webapp/channels/src/components/root/actions/index.ts
Обычный файл
173
webapp/channels/src/components/root/actions/index.ts
Обычный файл
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {History} from 'history';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {getClientConfig, getLicenseConfig, getFirstAdminSetupComplete} from 'mattermost-redux/actions/general';
|
||||
import {getServerLimits} from 'mattermost-redux/actions/limits';
|
||||
import {getMyPreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getMyTeamMembers, getMyTeams, getMyTeamUnreads} from 'mattermost-redux/actions/teams';
|
||||
import {getMe, getProfiles} from 'mattermost-redux/actions/users';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {isCollapsedThreadsEnabled, getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {checkIsFirstAdmin, getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import type {ActionFuncAsync, ThunkActionFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {redirectUserToDefaultTeam, emitUserLoggedOutEvent} from 'actions/global_actions';
|
||||
|
||||
import {ActionTypes, StoragePrefixes} from 'utils/constants';
|
||||
import {doesCookieContainsMMUserId} from 'utils/utils';
|
||||
|
||||
import type {Translations} from 'types/store/i18n';
|
||||
|
||||
export type TranslationPluginFunction = (locale: string) => Translations
|
||||
|
||||
/**
|
||||
* This function meant to be used in root.tsx component loads config, license and if user is logged in, it loads user and its related data.
|
||||
*/
|
||||
export function loadConfigAndMe(): ThunkActionFunc<Promise<{isLoaded: boolean; isMeRequested?: boolean}>> {
|
||||
return async (dispatch, getState) => {
|
||||
// attempt to load config and license regardless if user is logged in or not
|
||||
try {
|
||||
await Promise.all([
|
||||
dispatch(getClientConfig()),
|
||||
dispatch(getLicenseConfig()),
|
||||
]);
|
||||
} catch (error) {
|
||||
dispatch(logError(error as ServerError));
|
||||
return {
|
||||
isLoaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Return early if user is not logged in
|
||||
if (!doesCookieContainsMMUserId()) {
|
||||
return {
|
||||
isLoaded: true,
|
||||
isMeRequested: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Load user and its related data now that we know that user is logged in
|
||||
const serverVersion = getState().entities.general.serverVersion || Client4.getServerVersion();
|
||||
dispatch({type: GeneralTypes.RECEIVED_SERVER_VERSION, data: serverVersion});
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
dispatch(getMe()),
|
||||
dispatch(getMyPreferences()),
|
||||
dispatch(getMyTeams()),
|
||||
dispatch(getMyTeamMembers()),
|
||||
]);
|
||||
|
||||
dispatch(getMyTeamUnreads(isCollapsedThreadsEnabled(getState())));
|
||||
dispatch(getServerLimits());
|
||||
} catch (error) {
|
||||
dispatch(logError(error as ServerError));
|
||||
return {
|
||||
isLoaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isLoaded: true,
|
||||
isMeRequested: true,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function registerCustomPostRenderer(type: string, component: any, id: string): ActionFuncAsync {
|
||||
return async (dispatch) => {
|
||||
// piggyback on plugins state to register a custom post renderer
|
||||
dispatch({
|
||||
type: ActionTypes.RECEIVED_PLUGIN_POST_COMPONENT,
|
||||
data: {
|
||||
postTypeId: id,
|
||||
pluginId: id,
|
||||
type,
|
||||
component,
|
||||
},
|
||||
});
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function redirectToOnboardingOrDefaultTeam(history: History): ThunkActionFunc<void> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const isUserAdmin = isCurrentUserSystemAdmin(state);
|
||||
if (!isUserAdmin) {
|
||||
redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
|
||||
const teams = getActiveTeamsList(state);
|
||||
|
||||
const onboardingFlowEnabled = getIsOnboardingFlowEnabled(state);
|
||||
|
||||
if (teams.length > 0 || !onboardingFlowEnabled) {
|
||||
redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
|
||||
const firstAdminSetupComplete = await dispatch(getFirstAdminSetupComplete());
|
||||
if (firstAdminSetupComplete?.data) {
|
||||
redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
|
||||
const profilesResult = await dispatch(getProfiles(0, General.PROFILE_CHUNK_SIZE, {roles: General.SYSTEM_ADMIN_ROLE}));
|
||||
if (profilesResult.error) {
|
||||
redirectUserToDefaultTeam();
|
||||
return;
|
||||
}
|
||||
const currentUser = getCurrentUser(getState());
|
||||
const adminProfiles = profilesResult.data?.reduce(
|
||||
(acc: Record<string, UserProfile>, curr: UserProfile) => {
|
||||
acc[curr.id] = curr;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
if (adminProfiles && checkIsFirstAdmin(currentUser, adminProfiles)) {
|
||||
history.push('/preparing-workspace');
|
||||
return;
|
||||
}
|
||||
|
||||
redirectUserToDefaultTeam();
|
||||
};
|
||||
}
|
||||
|
||||
export function handleLoginLogoutSignal(e: StorageEvent): ThunkActionFunc<void> {
|
||||
return (dispatch, getState) => {
|
||||
// when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
|
||||
const isNewLocalStorageEvent = (event: StorageEvent) => event.storageArea === localStorage && event.newValue;
|
||||
|
||||
if (e.key === StoragePrefixes.LOGOUT && isNewLocalStorageEvent(e)) {
|
||||
console.log('detected logout from a different tab'); //eslint-disable-line no-console
|
||||
emitUserLoggedOutEvent('/', false, false);
|
||||
}
|
||||
if (e.key === StoragePrefixes.LOGIN && isNewLocalStorageEvent(e)) {
|
||||
const isLoggedIn = getCurrentUser(getState());
|
||||
|
||||
// make sure this is not the same tab which sent login signal
|
||||
// because another tabs will also send login signal after reloading
|
||||
if (isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// detected login from a different tab
|
||||
function reloadOnFocus() {
|
||||
location.reload();
|
||||
}
|
||||
window.addEventListener('focus', reloadOnFocus);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {withRouter} from 'react-router-dom';
|
||||
import {bindActionCreators} from 'redux';
|
||||
@@ -15,7 +17,6 @@ import {getTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {shouldShowTermsOfService, getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {loadRecentlyUsedCustomEmojis, migrateRecentEmojis} from 'actions/emoji_actions';
|
||||
import {loadConfigAndMe, registerCustomPostRenderer} from 'actions/views/root';
|
||||
import {getShowLaunchingWorkspace} from 'selectors/onboarding';
|
||||
import {shouldShowAppBar} from 'selectors/plugins';
|
||||
import {
|
||||
@@ -29,7 +30,12 @@ import {initializeProducts} from 'plugins/products';
|
||||
|
||||
import type {GlobalState} from 'types/store/index';
|
||||
|
||||
import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions';
|
||||
import {
|
||||
loadConfigAndMe,
|
||||
registerCustomPostRenderer,
|
||||
handleLoginLogoutSignal,
|
||||
redirectToOnboardingOrDefaultTeam,
|
||||
} from './actions';
|
||||
import Root from './root';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
@@ -42,11 +48,16 @@ function mapStateToProps(state: GlobalState) {
|
||||
const teamId = LocalStorageStore.getPreviousTeamId(userId);
|
||||
const permalinkRedirectTeam = getTeam(state, teamId!);
|
||||
|
||||
const isConfigLoaded = config && !isEmpty(config);
|
||||
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
isConfigLoaded,
|
||||
telemetryEnabled: config.DiagnosticsEnabled === 'true',
|
||||
noAccounts: config.NoAccounts === 'true',
|
||||
telemetryId: config.DiagnosticId,
|
||||
serviceEnvironment: config.ServiceEnvironment,
|
||||
siteURL: config.SiteURL,
|
||||
iosDownloadLink: config.IosAppDownloadLink,
|
||||
androidDownloadLink: config.AndroidAppDownloadLink,
|
||||
appDownloadLink: config.AppDownloadLink,
|
||||
@@ -79,4 +90,8 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Root));
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default withRouter(connector(Root));
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import type {RouteComponentProps} from 'react-router-dom';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import rudderAnalytics from 'rudder-sdk-js';
|
||||
|
||||
import {ServiceEnvironment} from '@mattermost/types/config';
|
||||
|
||||
@@ -14,40 +12,56 @@ import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions';
|
||||
|
||||
import Root from 'components/root/root';
|
||||
|
||||
import testConfigureStore from 'packages/mattermost-redux/test/test_store';
|
||||
import {renderWithContext, waitFor} from 'tests/react_testing_utils';
|
||||
import {StoragePrefixes} from 'utils/constants';
|
||||
|
||||
import type {ProductComponent} from 'types/store/plugins';
|
||||
|
||||
import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions';
|
||||
import type {Props} from './root';
|
||||
import Root from './root';
|
||||
|
||||
jest.mock('rudder-sdk-js', () => ({
|
||||
identify: jest.fn(),
|
||||
load: jest.fn(),
|
||||
page: jest.fn(),
|
||||
ready: jest.fn((callback) => callback()),
|
||||
track: jest.fn(),
|
||||
jest.mock('mattermost-redux/client/rudder', () => ({
|
||||
rudderAnalytics: {
|
||||
identify: jest.fn(),
|
||||
load: jest.fn(),
|
||||
page: jest.fn(),
|
||||
ready: jest.fn((callback) => callback()), // Default behavior: calls the callback
|
||||
track: jest.fn(),
|
||||
},
|
||||
RudderTelemetryHandler: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('mattermost-redux/client/rudder', () => {
|
||||
const actual = jest.requireActual('mattermost-redux/client/rudder');
|
||||
return {
|
||||
...actual,
|
||||
rudderAnalytics: {
|
||||
...actual.rudderAnalytics,
|
||||
ready: jest.fn((callback) => callback()),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('actions/telemetry_actions');
|
||||
|
||||
jest.mock('actions/global_actions', () => ({
|
||||
redirectUserToDefaultTeam: jest.fn(),
|
||||
}));
|
||||
jest.mock('components/announcement_bar', () => () => <div/>);
|
||||
jest.mock('components/team_sidebar', () => () => <div/>);
|
||||
jest.mock('components/mobile_view_watcher', () => () => <div/>);
|
||||
jest.mock('./performance_reporter_controller', () => () => <div/>);
|
||||
|
||||
jest.mock('utils/utils', () => {
|
||||
const original = jest.requireActual('utils/utils');
|
||||
|
||||
return {
|
||||
...original,
|
||||
localizeMessage: () => {},
|
||||
applyTheme: jest.fn(),
|
||||
makeIsEligibleForClick: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('actions/global_actions', () => ({
|
||||
redirectUserToDefaultTeam: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('mattermost-redux/actions/general', () => ({
|
||||
getFirstAdminSetupComplete: jest.fn(() => Promise.resolve({
|
||||
type: 'FIRST_ADMIN_COMPLETE_SETUP_RECEIVED',
|
||||
@@ -59,23 +73,37 @@ jest.mock('mattermost-redux/actions/general', () => ({
|
||||
describe('components/Root', () => {
|
||||
const store = testConfigureStore();
|
||||
|
||||
const baseProps = {
|
||||
telemetryEnabled: true,
|
||||
telemetryId: '1234ab',
|
||||
noAccounts: false,
|
||||
showTermsOfService: false,
|
||||
const baseProps: Props = {
|
||||
theme: {} as Theme,
|
||||
isConfigLoaded: true,
|
||||
telemetryEnabled: true,
|
||||
noAccounts: false,
|
||||
telemetryId: '1234ab',
|
||||
serviceEnvironment: undefined,
|
||||
siteURL: 'http://localhost:8065',
|
||||
iosDownloadLink: undefined,
|
||||
androidDownloadLink: undefined,
|
||||
appDownloadLink: undefined,
|
||||
showTermsOfService: false,
|
||||
plugins: [],
|
||||
products: [],
|
||||
showLaunchingWorkspace: false,
|
||||
rhsIsExpanded: false,
|
||||
rhsIsOpen: false,
|
||||
rhsState: null,
|
||||
shouldShowAppBar: false,
|
||||
isCloud: false,
|
||||
actions: {
|
||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
config: {},
|
||||
isMeLoaded: false,
|
||||
isLoaded: true,
|
||||
isMeRequested: false,
|
||||
});
|
||||
}),
|
||||
getFirstAdminSetupComplete: jest.fn(),
|
||||
getProfiles: jest.fn(),
|
||||
loadRecentlyUsedCustomEmojis: jest.fn(),
|
||||
migrateRecentEmojis: jest.fn(),
|
||||
savePreferences: jest.fn(),
|
||||
registerCustomPostRenderer: jest.fn(),
|
||||
initializeProducts: jest.fn(),
|
||||
...bindActionCreators({
|
||||
@@ -84,37 +112,62 @@ describe('components/Root', () => {
|
||||
}, store.dispatch),
|
||||
},
|
||||
permalinkRedirectTeamName: 'myTeam',
|
||||
showLaunchingWorkspace: false,
|
||||
plugins: [],
|
||||
products: [],
|
||||
...{
|
||||
location: {
|
||||
pathname: '/',
|
||||
},
|
||||
} as RouteComponentProps,
|
||||
isCloud: false,
|
||||
rhsIsExpanded: false,
|
||||
rhsIsOpen: false,
|
||||
shouldShowAppBar: false,
|
||||
};
|
||||
|
||||
test('should load config and license on mount and redirect to sign-up page', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
noAccounts: true,
|
||||
history: {
|
||||
push: jest.fn(),
|
||||
} as unknown as RouteComponentProps['history'],
|
||||
};
|
||||
} as RouteComponentProps,
|
||||
};
|
||||
|
||||
const wrapper = shallow<Root>(<Root {...props}/>);
|
||||
let originalMatchMedia: (query: string) => MediaQueryList;
|
||||
let originalReload: () => void;
|
||||
|
||||
wrapper.instance().onConfigLoaded({});
|
||||
expect(props.history.push).toHaveBeenCalledWith('/signup_user_complete');
|
||||
wrapper.unmount();
|
||||
beforeAll(() => {
|
||||
originalMatchMedia = window.matchMedia;
|
||||
originalReload = window.location.reload;
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
})),
|
||||
});
|
||||
|
||||
Object.defineProperty(window.location, 'reload', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
window.location.reload = jest.fn();
|
||||
});
|
||||
|
||||
test('should load user, config, and license on mount and redirect to defaultTeam on success', (done) => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
window.matchMedia = originalMatchMedia;
|
||||
window.location.reload = originalReload;
|
||||
});
|
||||
|
||||
test('should load config and license on mount and redirect to sign-up page', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
noAccounts: true,
|
||||
};
|
||||
|
||||
renderWithContext(<Root {...props}/>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.history.push).toHaveBeenCalledWith('/signup_user_complete');
|
||||
});
|
||||
});
|
||||
|
||||
test('should load user, config, and license on mount and redirect to defaultTeam on success', async () => {
|
||||
document.cookie = 'MMUSERID=userid';
|
||||
localStorage.setItem('was_logged_in', 'true');
|
||||
|
||||
@@ -124,28 +177,22 @@ describe('components/Root', () => {
|
||||
...baseProps.actions,
|
||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
config: {},
|
||||
isMeLoaded: true,
|
||||
isLoaded: true,
|
||||
isMeRequested: true,
|
||||
});
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the method by extending the class because we don't have a chance to do it before shallow mounts the component
|
||||
class MockedRoot extends Root {
|
||||
onConfigLoaded = jest.fn(() => {
|
||||
expect(this.onConfigLoaded).toHaveBeenCalledTimes(1);
|
||||
expect(GlobalActions.redirectUserToDefaultTeam).toHaveBeenCalledTimes(1);
|
||||
expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
});
|
||||
}
|
||||
renderWithContext(<Root {...props}/>);
|
||||
|
||||
const wrapper = shallow(<MockedRoot {...props}/>);
|
||||
wrapper.unmount();
|
||||
await waitFor(() => {
|
||||
expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1);
|
||||
expect(GlobalActions.redirectUserToDefaultTeam).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should load user, config, and license on mount and should not redirect to defaultTeam id pathname is not root', (done) => {
|
||||
test('should load user, config, and license on mount and should not redirect to defaultTeam id pathname is not root', async () => {
|
||||
document.cookie = 'MMUSERID=userid';
|
||||
localStorage.setItem('was_logged_in', 'true');
|
||||
|
||||
@@ -158,25 +205,19 @@ describe('components/Root', () => {
|
||||
...baseProps.actions,
|
||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
config: {},
|
||||
isMeLoaded: true,
|
||||
isLoaded: true,
|
||||
isMeRequested: true,
|
||||
});
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the method by extending the class because we don't have a chance to do it before shallow mounts the component
|
||||
class MockedRoot extends Root {
|
||||
onConfigLoaded = jest.fn(() => {
|
||||
expect(this.onConfigLoaded).toHaveBeenCalledTimes(1);
|
||||
expect(GlobalActions.redirectUserToDefaultTeam).not.toHaveBeenCalled();
|
||||
expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
});
|
||||
}
|
||||
renderWithContext(<Root {...props}/>);
|
||||
|
||||
const wrapper = shallow(<MockedRoot {...props}/>);
|
||||
wrapper.unmount();
|
||||
await waitFor(() => {
|
||||
expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1);
|
||||
expect(GlobalActions.redirectUserToDefaultTeam).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should call history on props change', () => {
|
||||
@@ -187,23 +228,24 @@ describe('components/Root', () => {
|
||||
push: jest.fn(),
|
||||
} as unknown as RouteComponentProps['history'],
|
||||
};
|
||||
const wrapper = shallow<Root>(<Root {...props}/>);
|
||||
|
||||
const {rerender} = renderWithContext(<Root {...props}/>);
|
||||
|
||||
expect(props.history.push).not.toHaveBeenCalled();
|
||||
|
||||
const props2 = {
|
||||
...props,
|
||||
noAccounts: true,
|
||||
};
|
||||
wrapper.setProps(props2);
|
||||
|
||||
rerender(<Root {...props2}/>);
|
||||
|
||||
expect(props.history.push).toHaveBeenLastCalledWith('/signup_user_complete');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should reload on focus after getting signal login event from another tab', () => {
|
||||
Object.defineProperty(window.location, 'reload', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
window.location.reload = jest.fn();
|
||||
const wrapper = shallow<Root>(<Root {...baseProps}/>);
|
||||
renderWithContext(<Root {...baseProps}/>);
|
||||
|
||||
const loginSignal = new StorageEvent('storage', {
|
||||
key: StoragePrefixes.LOGIN,
|
||||
newValue: String(Math.random()),
|
||||
@@ -212,89 +254,68 @@ describe('components/Root', () => {
|
||||
|
||||
window.dispatchEvent(loginSignal);
|
||||
window.dispatchEvent(new Event('focus'));
|
||||
|
||||
expect(window.location.reload).toBeCalledTimes(1);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
describe('onConfigLoaded', () => {
|
||||
afterEach(() => {
|
||||
Client4.telemetryHandler = undefined;
|
||||
test('should not set a TelemetryHandler when onConfigLoaded is called if Rudder is not configured', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
serviceEnvironment: ServiceEnvironment.DEV,
|
||||
actions: {
|
||||
...baseProps.actions,
|
||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
isLoaded: true,
|
||||
isMeRequested: true,
|
||||
});
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(<Root {...props}/>);
|
||||
|
||||
// Wait for the component to load config and call onConfigLoaded
|
||||
await waitFor(() => {
|
||||
expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should not set a TelemetryHandler when onConfigLoaded is called if Rudder is not configured', () => {
|
||||
const wrapper = shallow<Root>(<Root {...baseProps}/>);
|
||||
Client4.trackEvent('category', 'event');
|
||||
|
||||
wrapper.instance().onConfigLoaded({
|
||||
ServiceEnvironment: ServiceEnvironment.DEV,
|
||||
});
|
||||
|
||||
Client4.trackEvent('category', 'event');
|
||||
|
||||
expect(Client4.telemetryHandler).not.toBeDefined();
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should set a TelemetryHandler when onConfigLoaded is called if Rudder is configured', () => {
|
||||
const wrapper = shallow<Root>(<Root {...baseProps}/>);
|
||||
|
||||
wrapper.instance().onConfigLoaded({
|
||||
ServiceEnvironment: ServiceEnvironment.TEST,
|
||||
});
|
||||
|
||||
Client4.trackEvent('category', 'event');
|
||||
|
||||
expect(Client4.telemetryHandler).toBeDefined();
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should not set a TelemetryHandler when onConfigLoaded is called but Rudder has been blocked', () => {
|
||||
(rudderAnalytics.ready as any).mockImplementation(() => {
|
||||
// Simulate an error occurring and the callback not getting called
|
||||
});
|
||||
|
||||
const wrapper = shallow<Root>(<Root {...baseProps}/>);
|
||||
|
||||
wrapper.instance().onConfigLoaded({
|
||||
ServiceEnvironment: ServiceEnvironment.PRODUCTION,
|
||||
});
|
||||
|
||||
Client4.trackEvent('category', 'event');
|
||||
|
||||
expect(Client4.telemetryHandler).not.toBeDefined();
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
expect(Client4.telemetryHandler).not.toBeDefined();
|
||||
});
|
||||
|
||||
describe('Routes', () => {
|
||||
test('Should mount public product routes', () => {
|
||||
const mainComponent = () => (<p>{'TestMainComponent'}</p>);
|
||||
const publicComponent = () => (<p>{'TestPublicProduct'}</p>);
|
||||
test('should set a TelemetryHandler when onConfigLoaded is called if Rudder is configured', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isConfigLoaded: false,
|
||||
serviceEnvironment: ServiceEnvironment.TEST,
|
||||
actions: {
|
||||
...baseProps.actions,
|
||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
isLoaded: true,
|
||||
isMeRequested: true,
|
||||
});
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
products: [{
|
||||
id: 'productwithpublic',
|
||||
baseURL: '/productwithpublic',
|
||||
mainComponent,
|
||||
publicComponent,
|
||||
} as unknown as ProductComponent,
|
||||
{
|
||||
id: 'productwithoutpublic',
|
||||
baseURL: '/productwithoutpublic',
|
||||
mainComponent,
|
||||
publicComponent: null,
|
||||
} as unknown as ProductComponent],
|
||||
};
|
||||
const {rerender} = renderWithContext(<Root {...props}/>);
|
||||
|
||||
const wrapper = shallow<Root>(<Root {...props}/>);
|
||||
|
||||
wrapper.instance().setState({configLoaded: true});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
wrapper.unmount();
|
||||
// Wait for the component to load config and call onConfigLoaded
|
||||
await waitFor(() => {
|
||||
expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const props2 = {
|
||||
...props,
|
||||
isConfigLoaded: true,
|
||||
};
|
||||
|
||||
rerender(<Root {...props2}/>);
|
||||
|
||||
expect(Client4.telemetryHandler).toBeDefined();
|
||||
});
|
||||
|
||||
describe('showLandingPageIfNecessary', () => {
|
||||
@@ -309,19 +330,17 @@ describe('components/Root', () => {
|
||||
search: '',
|
||||
},
|
||||
} as RouteComponentProps,
|
||||
history: {
|
||||
push: jest.fn(),
|
||||
} as unknown as RouteComponentProps['history'],
|
||||
};
|
||||
|
||||
test('should show for normal cases', () => {
|
||||
const wrapper = shallow<Root>(<Root {...landingProps}/>);
|
||||
wrapper.instance().onConfigLoaded({});
|
||||
expect(landingProps.history.push).toHaveBeenCalledWith('/landing#/');
|
||||
wrapper.unmount();
|
||||
test('should show for normal cases', async () => {
|
||||
renderWithContext(<Root {...landingProps}/>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(landingProps.history.push).toHaveBeenCalledWith('/landing#/');
|
||||
});
|
||||
});
|
||||
|
||||
test('should not show for Desktop App login flow', () => {
|
||||
test('should not show for Desktop App login flow', async () => {
|
||||
const props = {
|
||||
...landingProps,
|
||||
...{
|
||||
@@ -330,10 +349,12 @@ describe('components/Root', () => {
|
||||
},
|
||||
} as RouteComponentProps,
|
||||
};
|
||||
const wrapper = shallow<Root>(<Root {...props}/>);
|
||||
wrapper.instance().onConfigLoaded({});
|
||||
expect(props.history.push).not.toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
|
||||
renderWithContext(<Root {...props}/>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.history.push).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
|
||||
import classNames from 'classnames';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import type {History} from 'history';
|
||||
import React from 'react';
|
||||
import {Route, Switch, Redirect} from 'react-router-dom';
|
||||
import type {RouteComponentProps} from 'react-router-dom';
|
||||
|
||||
import type {ClientConfig} from '@mattermost/types/config';
|
||||
import {ServiceEnvironment} from '@mattermost/types/config';
|
||||
|
||||
import {setSystemEmojis} from 'mattermost-redux/actions/emojis';
|
||||
@@ -16,7 +14,6 @@ import {setUrl} from 'mattermost-redux/actions/general';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder';
|
||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {measurePageLoadTelemetry, temporarilySetPageLoadContext, trackEvent, trackSelectorMetrics} from 'actions/telemetry_actions.jsx';
|
||||
import BrowserStore from 'stores/browser_store';
|
||||
@@ -52,13 +49,13 @@ import {getSiteURL} from 'utils/url';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
import type {ProductComponent, PluginComponent} from 'types/store/plugins';
|
||||
|
||||
import LuxonController from './luxon_controller';
|
||||
import PerformanceReporterController from './performance_reporter_controller';
|
||||
import RootProvider from './root_provider';
|
||||
import RootRedirect from './root_redirect';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
|
||||
import 'plugins/export.js';
|
||||
|
||||
const LazyErrorPage = React.lazy(() => import('components/error_page'));
|
||||
@@ -126,51 +123,19 @@ function LoggedInRoute(props: LoggedInRouteProps) {
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export type Actions = {
|
||||
getProfiles: (page?: number, pageSize?: number, options?: Record<string, any>) => Promise<ActionResult>;
|
||||
loadRecentlyUsedCustomEmojis: () => Promise<unknown>;
|
||||
migrateRecentEmojis: () => void;
|
||||
loadConfigAndMe: () => Promise<{config?: Partial<ClientConfig>; isMeLoaded: boolean}>;
|
||||
registerCustomPostRenderer: (type: string, component: any, id: string) => Promise<ActionResult>;
|
||||
initializeProducts: () => Promise<unknown>;
|
||||
handleLoginLogoutSignal: (e: StorageEvent) => unknown;
|
||||
redirectToOnboardingOrDefaultTeam: (history: History) => unknown;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
telemetryEnabled: boolean;
|
||||
telemetryId?: string;
|
||||
iosDownloadLink?: string;
|
||||
androidDownloadLink?: string;
|
||||
appDownloadLink?: string;
|
||||
noAccounts: boolean;
|
||||
showTermsOfService: boolean;
|
||||
permalinkRedirectTeamName: string;
|
||||
isCloud: boolean;
|
||||
actions: Actions;
|
||||
plugins?: PluginComponent[];
|
||||
products: ProductComponent[];
|
||||
showLaunchingWorkspace: boolean;
|
||||
rhsIsExpanded: boolean;
|
||||
rhsIsOpen: boolean;
|
||||
shouldShowAppBar: boolean;
|
||||
} & RouteComponentProps
|
||||
export type Props = PropsFromRedux & RouteComponentProps;
|
||||
|
||||
interface State {
|
||||
configLoaded?: boolean;
|
||||
shouldMountAppRoutes?: boolean;
|
||||
}
|
||||
|
||||
export default class Root extends React.PureComponent<Props, State> {
|
||||
private mounted: boolean;
|
||||
|
||||
// The constructor adds a bunch of event listeners,
|
||||
// so we do need this.
|
||||
private a11yController: A11yController;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.mounted = false;
|
||||
|
||||
// Redux
|
||||
setUrl(getSiteURL());
|
||||
@@ -199,18 +164,18 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
});
|
||||
|
||||
this.state = {
|
||||
configLoaded: false,
|
||||
shouldMountAppRoutes: false,
|
||||
};
|
||||
|
||||
this.a11yController = new A11yController();
|
||||
}
|
||||
|
||||
onConfigLoaded = (config: Partial<ClientConfig>) => {
|
||||
setRudderConfig = () => {
|
||||
const telemetryId = this.props.telemetryId;
|
||||
|
||||
const rudderUrl = 'https://pdat.matterlytics.com';
|
||||
let rudderKey = '';
|
||||
switch (config.ServiceEnvironment) {
|
||||
switch (this.props.serviceEnvironment) {
|
||||
case ServiceEnvironment.PRODUCTION:
|
||||
rudderKey = '1aoejPqhgONMI720CsBSRWzzRQ9';
|
||||
break;
|
||||
@@ -223,13 +188,15 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
|
||||
if (rudderKey !== '' && this.props.telemetryEnabled) {
|
||||
const rudderCfg: {setCookieDomain?: string} = {};
|
||||
const siteURL = config.SiteURL;
|
||||
if (siteURL !== '') {
|
||||
if (this.props.siteURL !== '') {
|
||||
try {
|
||||
rudderCfg.setCookieDomain = new URL(siteURL || '').hostname;
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (_) {}
|
||||
rudderCfg.setCookieDomain = new URL(this.props.siteURL || '').hostname;
|
||||
} catch (_) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to set cookie domain for RudderStack');
|
||||
}
|
||||
}
|
||||
|
||||
rudderAnalytics.load(rudderKey, rudderUrl || '', rudderCfg);
|
||||
|
||||
rudderAnalytics.identify(telemetryId, {}, {
|
||||
@@ -268,20 +235,14 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (this.props.location.pathname === '/' && this.props.noAccounts) {
|
||||
this.props.history.push('/signup_user_complete');
|
||||
}
|
||||
|
||||
onConfigLoaded = () => {
|
||||
Promise.all([
|
||||
this.props.actions.initializeProducts(),
|
||||
initializePlugins(),
|
||||
]).then(() => {
|
||||
if (this.mounted) {
|
||||
// supports enzyme tests, set state if and only if
|
||||
// the component is still mounted on screen
|
||||
this.setState({configLoaded: true});
|
||||
}
|
||||
this.setState({shouldMountAppRoutes: true});
|
||||
});
|
||||
|
||||
this.props.actions.migrateRecentEmojis();
|
||||
@@ -352,6 +313,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
if (!deepEqual(prevProps.theme, this.props.theme)) {
|
||||
Utils.applyTheme(this.props.theme);
|
||||
}
|
||||
|
||||
if (this.props.location.pathname === '/') {
|
||||
if (this.props.noAccounts) {
|
||||
prevProps.history.push('/signup_user_complete');
|
||||
@@ -359,6 +321,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
prevProps.history.push('/terms_of_service');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.shouldShowAppBar !== prevProps.shouldShowAppBar ||
|
||||
this.props.rhsIsOpen !== prevProps.rhsIsOpen ||
|
||||
@@ -366,6 +329,10 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
) {
|
||||
this.setRootMeta();
|
||||
}
|
||||
|
||||
if (!prevProps.isConfigLoaded && this.props.isConfigLoaded) {
|
||||
this.setRudderConfig();
|
||||
}
|
||||
}
|
||||
|
||||
captureUTMParams() {
|
||||
@@ -393,22 +360,26 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
initiateMeRequests = async () => {
|
||||
const {config, isMeLoaded} = await this.props.actions.loadConfigAndMe();
|
||||
const {isLoaded, isMeRequested} = await this.props.actions.loadConfigAndMe();
|
||||
|
||||
if (isMeLoaded && this.props.location.pathname === '/') {
|
||||
this.props.actions.redirectToOnboardingOrDefaultTeam(this.props.history);
|
||||
}
|
||||
if (isLoaded) {
|
||||
const isUserAtRootRoute = this.props.location.pathname === '/';
|
||||
|
||||
if (config) {
|
||||
this.onConfigLoaded(config);
|
||||
if (isUserAtRootRoute) {
|
||||
if (isMeRequested) {
|
||||
this.props.actions.redirectToOnboardingOrDefaultTeam(this.props.history);
|
||||
} else if (this.props.noAccounts) {
|
||||
this.props.history.push('/signup_user_complete');
|
||||
}
|
||||
}
|
||||
|
||||
this.onConfigLoaded();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
temporarilySetPageLoadContext(PageLoadContext.PAGE_LOAD);
|
||||
|
||||
this.mounted = true;
|
||||
|
||||
this.initiateMeRequests();
|
||||
|
||||
// See figma design on issue https://mattermost.atlassian.net/browse/MM-43649
|
||||
@@ -419,7 +390,6 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
window.removeEventListener('storage', this.handleLogoutLoginSignal);
|
||||
}
|
||||
|
||||
@@ -440,7 +410,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.configLoaded) {
|
||||
if (!this.state.shouldMountAppRoutes) {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,6 @@ export default keyMirror({
|
||||
GET_TEAMS_SUCCESS: null,
|
||||
GET_TEAMS_FAILURE: null,
|
||||
|
||||
MY_TEAMS_REQUEST: null,
|
||||
MY_TEAMS_SUCCESS: null,
|
||||
MY_TEAMS_FAILURE: null,
|
||||
|
||||
CREATE_TEAM_REQUEST: null,
|
||||
CREATE_TEAM_SUCCESS: null,
|
||||
CREATE_TEAM_FAILURE: null,
|
||||
|
||||
@@ -61,13 +61,8 @@ describe('Actions.Teams', () => {
|
||||
reply(200, [TestHelper.basicTeam]);
|
||||
await store.dispatch(Actions.getMyTeams());
|
||||
|
||||
const teamsRequest = store.getState().requests.teams.getMyTeams;
|
||||
const {teams} = store.getState().entities.teams;
|
||||
|
||||
if (teamsRequest.status === RequestStatus.FAILURE) {
|
||||
throw new Error(JSON.stringify(teamsRequest.error));
|
||||
}
|
||||
|
||||
expect(teams).toBeTruthy();
|
||||
expect(teams[TestHelper.basicTeam!.id]).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -66,9 +66,7 @@ export function selectTeam(team: Team | Team['id']) {
|
||||
export function getMyTeams() {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.getMyTeams,
|
||||
onRequest: TeamTypes.MY_TEAMS_REQUEST,
|
||||
onSuccess: [TeamTypes.RECEIVED_TEAMS_LIST, TeamTypes.MY_TEAMS_SUCCESS],
|
||||
onFailure: TeamTypes.MY_TEAMS_FAILURE,
|
||||
onSuccess: TeamTypes.RECEIVED_TEAMS_LIST,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,6 @@ import {TeamTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
import {handleRequest, initialRequestState} from './helpers';
|
||||
|
||||
function getMyTeams(state: RequestStatusType = initialRequestState(), action: AnyAction): RequestStatusType {
|
||||
return handleRequest(
|
||||
TeamTypes.MY_TEAMS_REQUEST,
|
||||
TeamTypes.MY_TEAMS_SUCCESS,
|
||||
TeamTypes.MY_TEAMS_FAILURE,
|
||||
state,
|
||||
action,
|
||||
);
|
||||
}
|
||||
|
||||
function getTeams(state: RequestStatusType = initialRequestState(), action: AnyAction): RequestStatusType {
|
||||
return handleRequest(
|
||||
TeamTypes.GET_TEAMS_REQUEST,
|
||||
@@ -32,5 +22,4 @@ function getTeams(state: RequestStatusType = initialRequestState(), action: AnyA
|
||||
|
||||
export default combineReducers({
|
||||
getTeams,
|
||||
getMyTeams,
|
||||
});
|
||||
|
||||
@@ -253,10 +253,6 @@ const state: GlobalState = {
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
getMyTeams: {
|
||||
status: 'not_started',
|
||||
error: null,
|
||||
},
|
||||
getTeams: {
|
||||
status: 'not_started',
|
||||
error: null,
|
||||
|
||||
@@ -1657,3 +1657,7 @@ export function sortUsersAndGroups(a: UserProfile | Group, b: UserProfile | Grou
|
||||
|
||||
return aSortString.localeCompare(bSortString);
|
||||
}
|
||||
|
||||
export function doesCookieContainsMMUserId() {
|
||||
return document.cookie.includes('MMUSERID=');
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ export type ThreadsRequestStatuses = {
|
||||
};
|
||||
|
||||
export type TeamsRequestsStatuses = {
|
||||
getMyTeams: RequestStatusType;
|
||||
getTeams: RequestStatusType;
|
||||
};
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user