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 mockStore from 'tests/test_store';
|
||||||
import {ActionTypes} from 'utils/constants';
|
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', () => {
|
describe('root view actions', () => {
|
||||||
const origCookies = document.cookie;
|
const origCookies = document.cookie;
|
||||||
const origWasLoggedIn = localStorage.getItem('was_logged_in');
|
const origWasLoggedIn = localStorage.getItem('was_logged_in');
|
||||||
@@ -38,25 +21,6 @@ describe('root view actions', () => {
|
|||||||
localStorage.setItem('was_logged_in', origWasLoggedIn || '');
|
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', () => {
|
describe('registerPluginTranslationsSource', () => {
|
||||||
test('Should not dispatch action when getTranslation is empty', () => {
|
test('Should not dispatch action when getTranslation is empty', () => {
|
||||||
const testStore = mockStore({});
|
const testStore = mockStore({});
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 {Client4} from 'mattermost-redux/client';
|
||||||
import type {ActionFuncAsync, ThunkActionFunc} from 'mattermost-redux/types/actions';
|
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 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> {
|
export function registerPluginTranslationsSource(pluginId: string, sourceFunction: TranslationPluginFunction): ThunkActionFunc<void, GlobalState> {
|
||||||
pluginTranslationSources[pluginId] = sourceFunction;
|
pluginTranslationSources[pluginId] = sourceFunction;
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
@@ -91,19 +67,3 @@ export function loadTranslations(locale: string, url: string): ActionFuncAsync {
|
|||||||
return {data: true};
|
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 DesktopApp from 'utils/desktop_api';
|
||||||
import {isKeyPressed} from 'utils/keyboard';
|
import {isKeyPressed} from 'utils/keyboard';
|
||||||
import {getBrowserTimezone} from 'utils/timezone';
|
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 {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -89,9 +90,9 @@ export default class LoggedIn extends React.PureComponent<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Device tracking setup
|
// Device tracking setup
|
||||||
if (UserAgent.isIos()) {
|
if (isIos()) {
|
||||||
document.body.classList.add('ios');
|
document.body.classList.add('ios');
|
||||||
} else if (UserAgent.isAndroid()) {
|
} else if (isAndroid()) {
|
||||||
document.body.classList.add('android');
|
document.body.classList.add('android');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +203,7 @@ export default class LoggedIn extends React.PureComponent<Props> {
|
|||||||
private handleBeforeUnload = (): void => {
|
private handleBeforeUnload = (): void => {
|
||||||
// remove the event listener to prevent getting stuck in a loop
|
// remove the event listener to prevent getting stuck in a loop
|
||||||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
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);
|
this.props.actions.updateApproximateViewTime(this.props.currentChannelId);
|
||||||
}
|
}
|
||||||
WebSocketActions.close();
|
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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import type {ConnectedProps} from 'react-redux';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import {withRouter} from 'react-router-dom';
|
import {withRouter} from 'react-router-dom';
|
||||||
import {bindActionCreators} from 'redux';
|
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 {shouldShowTermsOfService, getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {loadRecentlyUsedCustomEmojis, migrateRecentEmojis} from 'actions/emoji_actions';
|
import {loadRecentlyUsedCustomEmojis, migrateRecentEmojis} from 'actions/emoji_actions';
|
||||||
import {loadConfigAndMe, registerCustomPostRenderer} from 'actions/views/root';
|
|
||||||
import {getShowLaunchingWorkspace} from 'selectors/onboarding';
|
import {getShowLaunchingWorkspace} from 'selectors/onboarding';
|
||||||
import {shouldShowAppBar} from 'selectors/plugins';
|
import {shouldShowAppBar} from 'selectors/plugins';
|
||||||
import {
|
import {
|
||||||
@@ -29,7 +30,12 @@ import {initializeProducts} from 'plugins/products';
|
|||||||
|
|
||||||
import type {GlobalState} from 'types/store/index';
|
import type {GlobalState} from 'types/store/index';
|
||||||
|
|
||||||
import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions';
|
import {
|
||||||
|
loadConfigAndMe,
|
||||||
|
registerCustomPostRenderer,
|
||||||
|
handleLoginLogoutSignal,
|
||||||
|
redirectToOnboardingOrDefaultTeam,
|
||||||
|
} from './actions';
|
||||||
import Root from './root';
|
import Root from './root';
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
@@ -42,11 +48,16 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
const teamId = LocalStorageStore.getPreviousTeamId(userId);
|
const teamId = LocalStorageStore.getPreviousTeamId(userId);
|
||||||
const permalinkRedirectTeam = getTeam(state, teamId!);
|
const permalinkRedirectTeam = getTeam(state, teamId!);
|
||||||
|
|
||||||
|
const isConfigLoaded = config && !isEmpty(config);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: getTheme(state),
|
theme: getTheme(state),
|
||||||
|
isConfigLoaded,
|
||||||
telemetryEnabled: config.DiagnosticsEnabled === 'true',
|
telemetryEnabled: config.DiagnosticsEnabled === 'true',
|
||||||
noAccounts: config.NoAccounts === 'true',
|
noAccounts: config.NoAccounts === 'true',
|
||||||
telemetryId: config.DiagnosticId,
|
telemetryId: config.DiagnosticId,
|
||||||
|
serviceEnvironment: config.ServiceEnvironment,
|
||||||
|
siteURL: config.SiteURL,
|
||||||
iosDownloadLink: config.IosAppDownloadLink,
|
iosDownloadLink: config.IosAppDownloadLink,
|
||||||
androidDownloadLink: config.AndroidAppDownloadLink,
|
androidDownloadLink: config.AndroidAppDownloadLink,
|
||||||
appDownloadLink: config.AppDownloadLink,
|
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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {shallow} from 'enzyme';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type {RouteComponentProps} from 'react-router-dom';
|
import type {RouteComponentProps} from 'react-router-dom';
|
||||||
import {bindActionCreators} from 'redux';
|
import {bindActionCreators} from 'redux';
|
||||||
import rudderAnalytics from 'rudder-sdk-js';
|
|
||||||
|
|
||||||
import {ServiceEnvironment} from '@mattermost/types/config';
|
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 * as GlobalActions from 'actions/global_actions';
|
||||||
|
|
||||||
import Root from 'components/root/root';
|
|
||||||
|
|
||||||
import testConfigureStore from 'packages/mattermost-redux/test/test_store';
|
import testConfigureStore from 'packages/mattermost-redux/test/test_store';
|
||||||
|
import {renderWithContext, waitFor} from 'tests/react_testing_utils';
|
||||||
import {StoragePrefixes} from 'utils/constants';
|
import {StoragePrefixes} from 'utils/constants';
|
||||||
|
|
||||||
import type {ProductComponent} from 'types/store/plugins';
|
|
||||||
|
|
||||||
import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions';
|
import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions';
|
||||||
|
import type {Props} from './root';
|
||||||
|
import Root from './root';
|
||||||
|
|
||||||
jest.mock('rudder-sdk-js', () => ({
|
jest.mock('mattermost-redux/client/rudder', () => ({
|
||||||
identify: jest.fn(),
|
rudderAnalytics: {
|
||||||
load: jest.fn(),
|
identify: jest.fn(),
|
||||||
page: jest.fn(),
|
load: jest.fn(),
|
||||||
ready: jest.fn((callback) => callback()),
|
page: jest.fn(),
|
||||||
track: 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/telemetry_actions');
|
||||||
|
|
||||||
jest.mock('actions/global_actions', () => ({
|
jest.mock('components/announcement_bar', () => () => <div/>);
|
||||||
redirectUserToDefaultTeam: jest.fn(),
|
jest.mock('components/team_sidebar', () => () => <div/>);
|
||||||
}));
|
jest.mock('components/mobile_view_watcher', () => () => <div/>);
|
||||||
|
jest.mock('./performance_reporter_controller', () => () => <div/>);
|
||||||
|
|
||||||
jest.mock('utils/utils', () => {
|
jest.mock('utils/utils', () => {
|
||||||
const original = jest.requireActual('utils/utils');
|
const original = jest.requireActual('utils/utils');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...original,
|
...original,
|
||||||
localizeMessage: () => {},
|
|
||||||
applyTheme: jest.fn(),
|
applyTheme: jest.fn(),
|
||||||
makeIsEligibleForClick: jest.fn(),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('actions/global_actions', () => ({
|
||||||
|
redirectUserToDefaultTeam: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('mattermost-redux/actions/general', () => ({
|
jest.mock('mattermost-redux/actions/general', () => ({
|
||||||
getFirstAdminSetupComplete: jest.fn(() => Promise.resolve({
|
getFirstAdminSetupComplete: jest.fn(() => Promise.resolve({
|
||||||
type: 'FIRST_ADMIN_COMPLETE_SETUP_RECEIVED',
|
type: 'FIRST_ADMIN_COMPLETE_SETUP_RECEIVED',
|
||||||
@@ -59,23 +73,37 @@ jest.mock('mattermost-redux/actions/general', () => ({
|
|||||||
describe('components/Root', () => {
|
describe('components/Root', () => {
|
||||||
const store = testConfigureStore();
|
const store = testConfigureStore();
|
||||||
|
|
||||||
const baseProps = {
|
const baseProps: Props = {
|
||||||
telemetryEnabled: true,
|
|
||||||
telemetryId: '1234ab',
|
|
||||||
noAccounts: false,
|
|
||||||
showTermsOfService: false,
|
|
||||||
theme: {} as Theme,
|
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: {
|
actions: {
|
||||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
config: {},
|
isLoaded: true,
|
||||||
isMeLoaded: false,
|
isMeRequested: false,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
getFirstAdminSetupComplete: jest.fn(),
|
||||||
getProfiles: jest.fn(),
|
getProfiles: jest.fn(),
|
||||||
loadRecentlyUsedCustomEmojis: jest.fn(),
|
loadRecentlyUsedCustomEmojis: jest.fn(),
|
||||||
migrateRecentEmojis: jest.fn(),
|
migrateRecentEmojis: jest.fn(),
|
||||||
savePreferences: jest.fn(),
|
|
||||||
registerCustomPostRenderer: jest.fn(),
|
registerCustomPostRenderer: jest.fn(),
|
||||||
initializeProducts: jest.fn(),
|
initializeProducts: jest.fn(),
|
||||||
...bindActionCreators({
|
...bindActionCreators({
|
||||||
@@ -84,37 +112,62 @@ describe('components/Root', () => {
|
|||||||
}, store.dispatch),
|
}, store.dispatch),
|
||||||
},
|
},
|
||||||
permalinkRedirectTeamName: 'myTeam',
|
permalinkRedirectTeamName: 'myTeam',
|
||||||
showLaunchingWorkspace: false,
|
|
||||||
plugins: [],
|
|
||||||
products: [],
|
|
||||||
...{
|
...{
|
||||||
location: {
|
location: {
|
||||||
pathname: '/',
|
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: {
|
history: {
|
||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
} as unknown as RouteComponentProps['history'],
|
} as unknown as RouteComponentProps['history'],
|
||||||
};
|
} as RouteComponentProps,
|
||||||
|
};
|
||||||
|
|
||||||
const wrapper = shallow<Root>(<Root {...props}/>);
|
let originalMatchMedia: (query: string) => MediaQueryList;
|
||||||
|
let originalReload: () => void;
|
||||||
|
|
||||||
wrapper.instance().onConfigLoaded({});
|
beforeAll(() => {
|
||||||
expect(props.history.push).toHaveBeenCalledWith('/signup_user_complete');
|
originalMatchMedia = window.matchMedia;
|
||||||
wrapper.unmount();
|
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';
|
document.cookie = 'MMUSERID=userid';
|
||||||
localStorage.setItem('was_logged_in', 'true');
|
localStorage.setItem('was_logged_in', 'true');
|
||||||
|
|
||||||
@@ -124,28 +177,22 @@ describe('components/Root', () => {
|
|||||||
...baseProps.actions,
|
...baseProps.actions,
|
||||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
config: {},
|
isLoaded: true,
|
||||||
isMeLoaded: 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
|
renderWithContext(<Root {...props}/>);
|
||||||
class MockedRoot extends Root {
|
|
||||||
onConfigLoaded = jest.fn(() => {
|
|
||||||
expect(this.onConfigLoaded).toHaveBeenCalledTimes(1);
|
|
||||||
expect(GlobalActions.redirectUserToDefaultTeam).toHaveBeenCalledTimes(1);
|
|
||||||
expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapper = shallow(<MockedRoot {...props}/>);
|
await waitFor(() => {
|
||||||
wrapper.unmount();
|
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';
|
document.cookie = 'MMUSERID=userid';
|
||||||
localStorage.setItem('was_logged_in', 'true');
|
localStorage.setItem('was_logged_in', 'true');
|
||||||
|
|
||||||
@@ -158,25 +205,19 @@ describe('components/Root', () => {
|
|||||||
...baseProps.actions,
|
...baseProps.actions,
|
||||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
config: {},
|
isLoaded: true,
|
||||||
isMeLoaded: 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
|
renderWithContext(<Root {...props}/>);
|
||||||
class MockedRoot extends Root {
|
|
||||||
onConfigLoaded = jest.fn(() => {
|
|
||||||
expect(this.onConfigLoaded).toHaveBeenCalledTimes(1);
|
|
||||||
expect(GlobalActions.redirectUserToDefaultTeam).not.toHaveBeenCalled();
|
|
||||||
expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapper = shallow(<MockedRoot {...props}/>);
|
await waitFor(() => {
|
||||||
wrapper.unmount();
|
expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(GlobalActions.redirectUserToDefaultTeam).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call history on props change', () => {
|
test('should call history on props change', () => {
|
||||||
@@ -187,23 +228,24 @@ describe('components/Root', () => {
|
|||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
} as unknown as RouteComponentProps['history'],
|
} as unknown as RouteComponentProps['history'],
|
||||||
};
|
};
|
||||||
const wrapper = shallow<Root>(<Root {...props}/>);
|
|
||||||
|
const {rerender} = renderWithContext(<Root {...props}/>);
|
||||||
|
|
||||||
expect(props.history.push).not.toHaveBeenCalled();
|
expect(props.history.push).not.toHaveBeenCalled();
|
||||||
|
|
||||||
const props2 = {
|
const props2 = {
|
||||||
|
...props,
|
||||||
noAccounts: true,
|
noAccounts: true,
|
||||||
};
|
};
|
||||||
wrapper.setProps(props2);
|
|
||||||
|
rerender(<Root {...props2}/>);
|
||||||
|
|
||||||
expect(props.history.push).toHaveBeenLastCalledWith('/signup_user_complete');
|
expect(props.history.push).toHaveBeenLastCalledWith('/signup_user_complete');
|
||||||
wrapper.unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reload on focus after getting signal login event from another tab', () => {
|
test('should reload on focus after getting signal login event from another tab', () => {
|
||||||
Object.defineProperty(window.location, 'reload', {
|
renderWithContext(<Root {...baseProps}/>);
|
||||||
configurable: true,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
window.location.reload = jest.fn();
|
|
||||||
const wrapper = shallow<Root>(<Root {...baseProps}/>);
|
|
||||||
const loginSignal = new StorageEvent('storage', {
|
const loginSignal = new StorageEvent('storage', {
|
||||||
key: StoragePrefixes.LOGIN,
|
key: StoragePrefixes.LOGIN,
|
||||||
newValue: String(Math.random()),
|
newValue: String(Math.random()),
|
||||||
@@ -212,89 +254,68 @@ describe('components/Root', () => {
|
|||||||
|
|
||||||
window.dispatchEvent(loginSignal);
|
window.dispatchEvent(loginSignal);
|
||||||
window.dispatchEvent(new Event('focus'));
|
window.dispatchEvent(new Event('focus'));
|
||||||
|
|
||||||
expect(window.location.reload).toBeCalledTimes(1);
|
expect(window.location.reload).toBeCalledTimes(1);
|
||||||
wrapper.unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onConfigLoaded', () => {
|
test('should not set a TelemetryHandler when onConfigLoaded is called if Rudder is not configured', async () => {
|
||||||
afterEach(() => {
|
const props = {
|
||||||
Client4.telemetryHandler = undefined;
|
...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', () => {
|
Client4.trackEvent('category', 'event');
|
||||||
const wrapper = shallow<Root>(<Root {...baseProps}/>);
|
|
||||||
|
|
||||||
wrapper.instance().onConfigLoaded({
|
expect(Client4.telemetryHandler).not.toBeDefined();
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Routes', () => {
|
test('should set a TelemetryHandler when onConfigLoaded is called if Rudder is configured', async () => {
|
||||||
test('Should mount public product routes', () => {
|
const props = {
|
||||||
const mainComponent = () => (<p>{'TestMainComponent'}</p>);
|
...baseProps,
|
||||||
const publicComponent = () => (<p>{'TestPublicProduct'}</p>);
|
isConfigLoaded: false,
|
||||||
|
serviceEnvironment: ServiceEnvironment.TEST,
|
||||||
|
actions: {
|
||||||
|
...baseProps.actions,
|
||||||
|
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
isLoaded: true,
|
||||||
|
isMeRequested: true,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const props = {
|
const {rerender} = renderWithContext(<Root {...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 wrapper = shallow<Root>(<Root {...props}/>);
|
// Wait for the component to load config and call onConfigLoaded
|
||||||
|
await waitFor(() => {
|
||||||
wrapper.instance().setState({configLoaded: true});
|
expect(props.actions.loadConfigAndMe).toHaveBeenCalledTimes(1);
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
wrapper.unmount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const props2 = {
|
||||||
|
...props,
|
||||||
|
isConfigLoaded: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
rerender(<Root {...props2}/>);
|
||||||
|
|
||||||
|
expect(Client4.telemetryHandler).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('showLandingPageIfNecessary', () => {
|
describe('showLandingPageIfNecessary', () => {
|
||||||
@@ -309,19 +330,17 @@ describe('components/Root', () => {
|
|||||||
search: '',
|
search: '',
|
||||||
},
|
},
|
||||||
} as RouteComponentProps,
|
} as RouteComponentProps,
|
||||||
history: {
|
|
||||||
push: jest.fn(),
|
|
||||||
} as unknown as RouteComponentProps['history'],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should show for normal cases', () => {
|
test('should show for normal cases', async () => {
|
||||||
const wrapper = shallow<Root>(<Root {...landingProps}/>);
|
renderWithContext(<Root {...landingProps}/>);
|
||||||
wrapper.instance().onConfigLoaded({});
|
|
||||||
expect(landingProps.history.push).toHaveBeenCalledWith('/landing#/');
|
await waitFor(() => {
|
||||||
wrapper.unmount();
|
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 = {
|
const props = {
|
||||||
...landingProps,
|
...landingProps,
|
||||||
...{
|
...{
|
||||||
@@ -330,10 +349,12 @@ describe('components/Root', () => {
|
|||||||
},
|
},
|
||||||
} as RouteComponentProps,
|
} as RouteComponentProps,
|
||||||
};
|
};
|
||||||
const wrapper = shallow<Root>(<Root {...props}/>);
|
|
||||||
wrapper.instance().onConfigLoaded({});
|
renderWithContext(<Root {...props}/>);
|
||||||
expect(props.history.push).not.toHaveBeenCalled();
|
|
||||||
wrapper.unmount();
|
await waitFor(() => {
|
||||||
|
expect(props.history.push).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,10 @@
|
|||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import deepEqual from 'fast-deep-equal';
|
import deepEqual from 'fast-deep-equal';
|
||||||
import type {History} from 'history';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Route, Switch, Redirect} from 'react-router-dom';
|
import {Route, Switch, Redirect} from 'react-router-dom';
|
||||||
import type {RouteComponentProps} 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 {ServiceEnvironment} from '@mattermost/types/config';
|
||||||
|
|
||||||
import {setSystemEmojis} from 'mattermost-redux/actions/emojis';
|
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 {Client4} from 'mattermost-redux/client';
|
||||||
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder';
|
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder';
|
||||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
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 {measurePageLoadTelemetry, temporarilySetPageLoadContext, trackEvent, trackSelectorMetrics} from 'actions/telemetry_actions.jsx';
|
||||||
import BrowserStore from 'stores/browser_store';
|
import BrowserStore from 'stores/browser_store';
|
||||||
@@ -52,13 +49,13 @@ import {getSiteURL} from 'utils/url';
|
|||||||
import * as UserAgent from 'utils/user_agent';
|
import * as UserAgent from 'utils/user_agent';
|
||||||
import * as Utils from 'utils/utils';
|
import * as Utils from 'utils/utils';
|
||||||
|
|
||||||
import type {ProductComponent, PluginComponent} from 'types/store/plugins';
|
|
||||||
|
|
||||||
import LuxonController from './luxon_controller';
|
import LuxonController from './luxon_controller';
|
||||||
import PerformanceReporterController from './performance_reporter_controller';
|
import PerformanceReporterController from './performance_reporter_controller';
|
||||||
import RootProvider from './root_provider';
|
import RootProvider from './root_provider';
|
||||||
import RootRedirect from './root_redirect';
|
import RootRedirect from './root_redirect';
|
||||||
|
|
||||||
|
import type {PropsFromRedux} from './index';
|
||||||
|
|
||||||
import 'plugins/export.js';
|
import 'plugins/export.js';
|
||||||
|
|
||||||
const LazyErrorPage = React.lazy(() => import('components/error_page'));
|
const LazyErrorPage = React.lazy(() => import('components/error_page'));
|
||||||
@@ -126,51 +123,19 @@ function LoggedInRoute(props: LoggedInRouteProps) {
|
|||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
export type Actions = {
|
export type Props = PropsFromRedux & RouteComponentProps;
|
||||||
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
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
configLoaded?: boolean;
|
shouldMountAppRoutes?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Root extends React.PureComponent<Props, State> {
|
export default class Root extends React.PureComponent<Props, State> {
|
||||||
private mounted: boolean;
|
|
||||||
|
|
||||||
// The constructor adds a bunch of event listeners,
|
// The constructor adds a bunch of event listeners,
|
||||||
// so we do need this.
|
// so we do need this.
|
||||||
private a11yController: A11yController;
|
private a11yController: A11yController;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.mounted = false;
|
|
||||||
|
|
||||||
// Redux
|
// Redux
|
||||||
setUrl(getSiteURL());
|
setUrl(getSiteURL());
|
||||||
@@ -199,18 +164,18 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
configLoaded: false,
|
shouldMountAppRoutes: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.a11yController = new A11yController();
|
this.a11yController = new A11yController();
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfigLoaded = (config: Partial<ClientConfig>) => {
|
setRudderConfig = () => {
|
||||||
const telemetryId = this.props.telemetryId;
|
const telemetryId = this.props.telemetryId;
|
||||||
|
|
||||||
const rudderUrl = 'https://pdat.matterlytics.com';
|
const rudderUrl = 'https://pdat.matterlytics.com';
|
||||||
let rudderKey = '';
|
let rudderKey = '';
|
||||||
switch (config.ServiceEnvironment) {
|
switch (this.props.serviceEnvironment) {
|
||||||
case ServiceEnvironment.PRODUCTION:
|
case ServiceEnvironment.PRODUCTION:
|
||||||
rudderKey = '1aoejPqhgONMI720CsBSRWzzRQ9';
|
rudderKey = '1aoejPqhgONMI720CsBSRWzzRQ9';
|
||||||
break;
|
break;
|
||||||
@@ -223,13 +188,15 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
if (rudderKey !== '' && this.props.telemetryEnabled) {
|
if (rudderKey !== '' && this.props.telemetryEnabled) {
|
||||||
const rudderCfg: {setCookieDomain?: string} = {};
|
const rudderCfg: {setCookieDomain?: string} = {};
|
||||||
const siteURL = config.SiteURL;
|
if (this.props.siteURL !== '') {
|
||||||
if (siteURL !== '') {
|
|
||||||
try {
|
try {
|
||||||
rudderCfg.setCookieDomain = new URL(siteURL || '').hostname;
|
rudderCfg.setCookieDomain = new URL(this.props.siteURL || '').hostname;
|
||||||
// eslint-disable-next-line no-empty
|
} catch (_) {
|
||||||
} catch (_) {}
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to set cookie domain for RudderStack');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rudderAnalytics.load(rudderKey, rudderUrl || '', rudderCfg);
|
rudderAnalytics.load(rudderKey, rudderUrl || '', rudderCfg);
|
||||||
|
|
||||||
rudderAnalytics.identify(telemetryId, {}, {
|
rudderAnalytics.identify(telemetryId, {}, {
|
||||||
@@ -268,20 +235,14 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (this.props.location.pathname === '/' && this.props.noAccounts) {
|
onConfigLoaded = () => {
|
||||||
this.props.history.push('/signup_user_complete');
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
this.props.actions.initializeProducts(),
|
this.props.actions.initializeProducts(),
|
||||||
initializePlugins(),
|
initializePlugins(),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
if (this.mounted) {
|
this.setState({shouldMountAppRoutes: true});
|
||||||
// supports enzyme tests, set state if and only if
|
|
||||||
// the component is still mounted on screen
|
|
||||||
this.setState({configLoaded: true});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.actions.migrateRecentEmojis();
|
this.props.actions.migrateRecentEmojis();
|
||||||
@@ -352,6 +313,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
if (!deepEqual(prevProps.theme, this.props.theme)) {
|
if (!deepEqual(prevProps.theme, this.props.theme)) {
|
||||||
Utils.applyTheme(this.props.theme);
|
Utils.applyTheme(this.props.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.location.pathname === '/') {
|
if (this.props.location.pathname === '/') {
|
||||||
if (this.props.noAccounts) {
|
if (this.props.noAccounts) {
|
||||||
prevProps.history.push('/signup_user_complete');
|
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');
|
prevProps.history.push('/terms_of_service');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.props.shouldShowAppBar !== prevProps.shouldShowAppBar ||
|
this.props.shouldShowAppBar !== prevProps.shouldShowAppBar ||
|
||||||
this.props.rhsIsOpen !== prevProps.rhsIsOpen ||
|
this.props.rhsIsOpen !== prevProps.rhsIsOpen ||
|
||||||
@@ -366,6 +329,10 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
) {
|
) {
|
||||||
this.setRootMeta();
|
this.setRootMeta();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!prevProps.isConfigLoaded && this.props.isConfigLoaded) {
|
||||||
|
this.setRudderConfig();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
captureUTMParams() {
|
captureUTMParams() {
|
||||||
@@ -393,22 +360,26 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initiateMeRequests = async () => {
|
initiateMeRequests = async () => {
|
||||||
const {config, isMeLoaded} = await this.props.actions.loadConfigAndMe();
|
const {isLoaded, isMeRequested} = await this.props.actions.loadConfigAndMe();
|
||||||
|
|
||||||
if (isMeLoaded && this.props.location.pathname === '/') {
|
if (isLoaded) {
|
||||||
this.props.actions.redirectToOnboardingOrDefaultTeam(this.props.history);
|
const isUserAtRootRoute = this.props.location.pathname === '/';
|
||||||
}
|
|
||||||
|
|
||||||
if (config) {
|
if (isUserAtRootRoute) {
|
||||||
this.onConfigLoaded(config);
|
if (isMeRequested) {
|
||||||
|
this.props.actions.redirectToOnboardingOrDefaultTeam(this.props.history);
|
||||||
|
} else if (this.props.noAccounts) {
|
||||||
|
this.props.history.push('/signup_user_complete');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onConfigLoaded();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
temporarilySetPageLoadContext(PageLoadContext.PAGE_LOAD);
|
temporarilySetPageLoadContext(PageLoadContext.PAGE_LOAD);
|
||||||
|
|
||||||
this.mounted = true;
|
|
||||||
|
|
||||||
this.initiateMeRequests();
|
this.initiateMeRequests();
|
||||||
|
|
||||||
// See figma design on issue https://mattermost.atlassian.net/browse/MM-43649
|
// 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() {
|
componentWillUnmount() {
|
||||||
this.mounted = false;
|
|
||||||
window.removeEventListener('storage', this.handleLogoutLoginSignal);
|
window.removeEventListener('storage', this.handleLogoutLoginSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +410,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.state.configLoaded) {
|
if (!this.state.shouldMountAppRoutes) {
|
||||||
return <div/>;
|
return <div/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ export default keyMirror({
|
|||||||
GET_TEAMS_SUCCESS: null,
|
GET_TEAMS_SUCCESS: null,
|
||||||
GET_TEAMS_FAILURE: null,
|
GET_TEAMS_FAILURE: null,
|
||||||
|
|
||||||
MY_TEAMS_REQUEST: null,
|
|
||||||
MY_TEAMS_SUCCESS: null,
|
|
||||||
MY_TEAMS_FAILURE: null,
|
|
||||||
|
|
||||||
CREATE_TEAM_REQUEST: null,
|
CREATE_TEAM_REQUEST: null,
|
||||||
CREATE_TEAM_SUCCESS: null,
|
CREATE_TEAM_SUCCESS: null,
|
||||||
CREATE_TEAM_FAILURE: null,
|
CREATE_TEAM_FAILURE: null,
|
||||||
|
|||||||
@@ -61,13 +61,8 @@ describe('Actions.Teams', () => {
|
|||||||
reply(200, [TestHelper.basicTeam]);
|
reply(200, [TestHelper.basicTeam]);
|
||||||
await store.dispatch(Actions.getMyTeams());
|
await store.dispatch(Actions.getMyTeams());
|
||||||
|
|
||||||
const teamsRequest = store.getState().requests.teams.getMyTeams;
|
|
||||||
const {teams} = store.getState().entities.teams;
|
const {teams} = store.getState().entities.teams;
|
||||||
|
|
||||||
if (teamsRequest.status === RequestStatus.FAILURE) {
|
|
||||||
throw new Error(JSON.stringify(teamsRequest.error));
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(teams).toBeTruthy();
|
expect(teams).toBeTruthy();
|
||||||
expect(teams[TestHelper.basicTeam!.id]).toBeTruthy();
|
expect(teams[TestHelper.basicTeam!.id]).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,9 +66,7 @@ export function selectTeam(team: Team | Team['id']) {
|
|||||||
export function getMyTeams() {
|
export function getMyTeams() {
|
||||||
return bindClientFunc({
|
return bindClientFunc({
|
||||||
clientFunc: Client4.getMyTeams,
|
clientFunc: Client4.getMyTeams,
|
||||||
onRequest: TeamTypes.MY_TEAMS_REQUEST,
|
onSuccess: TeamTypes.RECEIVED_TEAMS_LIST,
|
||||||
onSuccess: [TeamTypes.RECEIVED_TEAMS_LIST, TeamTypes.MY_TEAMS_SUCCESS],
|
|
||||||
onFailure: TeamTypes.MY_TEAMS_FAILURE,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,6 @@ import {TeamTypes} from 'mattermost-redux/action_types';
|
|||||||
|
|
||||||
import {handleRequest, initialRequestState} from './helpers';
|
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 {
|
function getTeams(state: RequestStatusType = initialRequestState(), action: AnyAction): RequestStatusType {
|
||||||
return handleRequest(
|
return handleRequest(
|
||||||
TeamTypes.GET_TEAMS_REQUEST,
|
TeamTypes.GET_TEAMS_REQUEST,
|
||||||
@@ -32,5 +22,4 @@ function getTeams(state: RequestStatusType = initialRequestState(), action: AnyA
|
|||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
getTeams,
|
getTeams,
|
||||||
getMyTeams,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -253,10 +253,6 @@ const state: GlobalState = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
teams: {
|
teams: {
|
||||||
getMyTeams: {
|
|
||||||
status: 'not_started',
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
getTeams: {
|
getTeams: {
|
||||||
status: 'not_started',
|
status: 'not_started',
|
||||||
error: null,
|
error: null,
|
||||||
|
|||||||
@@ -1657,3 +1657,7 @@ export function sortUsersAndGroups(a: UserProfile | Group, b: UserProfile | Grou
|
|||||||
|
|
||||||
return aSortString.localeCompare(bSortString);
|
return aSortString.localeCompare(bSortString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doesCookieContainsMMUserId() {
|
||||||
|
return document.cookie.includes('MMUSERID=');
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export type ThreadsRequestStatuses = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TeamsRequestsStatuses = {
|
export type TeamsRequestsStatuses = {
|
||||||
getMyTeams: RequestStatusType;
|
|
||||||
getTeams: RequestStatusType;
|
getTeams: RequestStatusType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user