mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Remove global state access from components/root (#26751)
* root part 1 - rudder * root part 2 - luxon * root part 3 - recent emojis * root part 4 - redirect to onboarding * root part 5 - login logout handler * Fix indentation
This commit is contained in:
parent
39ba2e72c0
commit
f3b80f77a6
@ -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 type {ClientConfig} from '@mattermost/types/config';
|
||||||
|
|
||||||
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
|
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
|
||||||
import {loadMe} from 'mattermost-redux/actions/users';
|
import {loadMe} from 'mattermost-redux/actions/users';
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
@ -18,9 +20,9 @@ const pluginTranslationSources: Record<string, TranslationPluginFunction> = {};
|
|||||||
|
|
||||||
export type TranslationPluginFunction = (locale: string) => Translations
|
export type TranslationPluginFunction = (locale: string) => Translations
|
||||||
|
|
||||||
export function loadConfigAndMe(): ActionFuncAsync<boolean> {
|
export function loadConfigAndMe(): ThunkActionFunc<Promise<{config?: ClientConfig; isMeLoaded: boolean}>> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
await Promise.all([
|
const results = await Promise.all([
|
||||||
dispatch(getClientConfig()),
|
dispatch(getClientConfig()),
|
||||||
dispatch(getLicenseConfig()),
|
dispatch(getLicenseConfig()),
|
||||||
]);
|
]);
|
||||||
@ -31,7 +33,10 @@ export function loadConfigAndMe(): ActionFuncAsync<boolean> {
|
|||||||
isMeLoaded = dataFromLoadMe?.data ?? false;
|
isMeLoaded = dataFromLoadMe?.data ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {data: isMeLoaded};
|
return {
|
||||||
|
config: results[0].data,
|
||||||
|
isMeLoaded,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
exports[`components/Root Routes Should mount public product routes 1`] = `
|
exports[`components/Root Routes Should mount public product routes 1`] = `
|
||||||
<RootProvider>
|
<RootProvider>
|
||||||
<Connect(MobileViewWatcher) />
|
<Connect(MobileViewWatcher) />
|
||||||
|
<LuxonController />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
component={[Function]}
|
component={[Function]}
|
||||||
|
91
webapp/channels/src/components/root/actions.ts
Normal file
91
webapp/channels/src/components/root/actions.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import {Settings} from 'luxon';
|
|
||||||
|
|
||||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
|
||||||
|
|
||||||
import {getCurrentLocale} from 'selectors/i18n';
|
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
|
||||||
|
|
||||||
let prevTimezone: string | undefined;
|
|
||||||
let prevLocale: string | undefined;
|
|
||||||
export function applyLuxonDefaults(state: GlobalState) {
|
|
||||||
const locale = getCurrentLocale(state);
|
|
||||||
if (locale !== prevLocale) {
|
|
||||||
prevLocale = locale;
|
|
||||||
Settings.defaultLocale = locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tz = getCurrentTimezone(state);
|
|
||||||
if (tz !== prevTimezone) {
|
|
||||||
prevTimezone = tz;
|
|
||||||
Settings.defaultZone = tz ?? 'system';
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,7 +13,7 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
|||||||
import {getTeam} from 'mattermost-redux/selectors/entities/teams';
|
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 {migrateRecentEmojis} from 'actions/emoji_actions';
|
import {loadRecentlyUsedCustomEmojis, migrateRecentEmojis} from 'actions/emoji_actions';
|
||||||
import {loadConfigAndMe, registerCustomPostRenderer} from 'actions/views/root';
|
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';
|
||||||
@ -28,6 +28,7 @@ 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 Root from './root';
|
import Root from './root';
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
@ -67,9 +68,12 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
loadConfigAndMe,
|
loadConfigAndMe,
|
||||||
getFirstAdminSetupComplete,
|
getFirstAdminSetupComplete,
|
||||||
getProfiles,
|
getProfiles,
|
||||||
|
loadRecentlyUsedCustomEmojis,
|
||||||
migrateRecentEmojis,
|
migrateRecentEmojis,
|
||||||
registerCustomPostRenderer,
|
registerCustomPostRenderer,
|
||||||
initializeProducts,
|
initializeProducts,
|
||||||
|
handleLoginLogoutSignal,
|
||||||
|
redirectToOnboardingOrDefaultTeam,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
25
webapp/channels/src/components/root/luxon_controller.tsx
Normal file
25
webapp/channels/src/components/root/luxon_controller.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Settings} from 'luxon';
|
||||||
|
import {useEffect} from 'react';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||||
|
|
||||||
|
import {getCurrentLocale} from 'selectors/i18n';
|
||||||
|
|
||||||
|
export default function LuxonController() {
|
||||||
|
const locale = useSelector(getCurrentLocale);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Settings.defaultLocale = locale;
|
||||||
|
}, [locale]);
|
||||||
|
|
||||||
|
const tz = useSelector(getCurrentTimezone);
|
||||||
|
useEffect(() => {
|
||||||
|
Settings.defaultZone = tz ?? 'system';
|
||||||
|
}, [tz]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
@ -4,23 +4,25 @@
|
|||||||
import {shallow} from 'enzyme';
|
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 rudderAnalytics from 'rudder-sdk-js';
|
import rudderAnalytics from 'rudder-sdk-js';
|
||||||
|
|
||||||
import {ServiceEnvironment} from '@mattermost/types/config';
|
import {ServiceEnvironment} from '@mattermost/types/config';
|
||||||
|
|
||||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions';
|
import * as GlobalActions from 'actions/global_actions';
|
||||||
import store from 'stores/redux_store';
|
|
||||||
|
|
||||||
import Root from 'components/root/root';
|
import Root from 'components/root/root';
|
||||||
|
|
||||||
|
import testConfigureStore from 'packages/mattermost-redux/test/test_store';
|
||||||
import {StoragePrefixes} from 'utils/constants';
|
import {StoragePrefixes} from 'utils/constants';
|
||||||
|
|
||||||
import type {ProductComponent} from 'types/store/plugins';
|
import type {ProductComponent} from 'types/store/plugins';
|
||||||
|
|
||||||
|
import {handleLoginLogoutSignal, redirectToOnboardingOrDefaultTeam} from './actions';
|
||||||
|
|
||||||
jest.mock('rudder-sdk-js', () => ({
|
jest.mock('rudder-sdk-js', () => ({
|
||||||
identify: jest.fn(),
|
identify: jest.fn(),
|
||||||
load: jest.fn(),
|
load: jest.fn(),
|
||||||
@ -43,15 +45,20 @@ jest.mock('utils/utils', () => {
|
|||||||
localizeMessage: () => {},
|
localizeMessage: () => {},
|
||||||
applyTheme: jest.fn(),
|
applyTheme: jest.fn(),
|
||||||
makeIsEligibleForClick: jest.fn(),
|
makeIsEligibleForClick: jest.fn(),
|
||||||
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('mattermost-redux/actions/general', () => ({
|
jest.mock('mattermost-redux/actions/general', () => ({
|
||||||
|
getFirstAdminSetupComplete: jest.fn(() => Promise.resolve({
|
||||||
|
type: 'FIRST_ADMIN_COMPLETE_SETUP_RECEIVED',
|
||||||
|
data: true,
|
||||||
|
})),
|
||||||
setUrl: () => {},
|
setUrl: () => {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('components/Root', () => {
|
describe('components/Root', () => {
|
||||||
|
const store = testConfigureStore();
|
||||||
|
|
||||||
const baseProps = {
|
const baseProps = {
|
||||||
telemetryEnabled: true,
|
telemetryEnabled: true,
|
||||||
telemetryId: '1234ab',
|
telemetryId: '1234ab',
|
||||||
@ -61,18 +68,20 @@ describe('components/Root', () => {
|
|||||||
actions: {
|
actions: {
|
||||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
data: false,
|
config: {},
|
||||||
|
isMeLoaded: false,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
getFirstAdminSetupComplete: jest.fn(() => Promise.resolve({
|
|
||||||
type: GeneralTypes.FIRST_ADMIN_COMPLETE_SETUP_RECEIVED,
|
|
||||||
data: true,
|
|
||||||
})),
|
|
||||||
getProfiles: jest.fn(),
|
getProfiles: jest.fn(),
|
||||||
|
loadRecentlyUsedCustomEmojis: jest.fn(),
|
||||||
migrateRecentEmojis: jest.fn(),
|
migrateRecentEmojis: jest.fn(),
|
||||||
savePreferences: jest.fn(),
|
savePreferences: jest.fn(),
|
||||||
registerCustomPostRenderer: jest.fn(),
|
registerCustomPostRenderer: jest.fn(),
|
||||||
initializeProducts: jest.fn(),
|
initializeProducts: jest.fn(),
|
||||||
|
...bindActionCreators({
|
||||||
|
handleLoginLogoutSignal,
|
||||||
|
redirectToOnboardingOrDefaultTeam,
|
||||||
|
}, store.dispatch),
|
||||||
},
|
},
|
||||||
permalinkRedirectTeamName: 'myTeam',
|
permalinkRedirectTeamName: 'myTeam',
|
||||||
showLaunchingWorkspace: false,
|
showLaunchingWorkspace: false,
|
||||||
@ -98,9 +107,9 @@ describe('components/Root', () => {
|
|||||||
} as unknown as RouteComponentProps['history'],
|
} as unknown as RouteComponentProps['history'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(<Root {...props}/>);
|
const wrapper = shallow<Root>(<Root {...props}/>);
|
||||||
|
|
||||||
(wrapper.instance() as any).onConfigLoaded();
|
wrapper.instance().onConfigLoaded({});
|
||||||
expect(props.history.push).toHaveBeenCalledWith('/signup_user_complete');
|
expect(props.history.push).toHaveBeenCalledWith('/signup_user_complete');
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
@ -114,7 +123,10 @@ describe('components/Root', () => {
|
|||||||
actions: {
|
actions: {
|
||||||
...baseProps.actions,
|
...baseProps.actions,
|
||||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve({data: true});
|
return Promise.resolve({
|
||||||
|
config: {},
|
||||||
|
isMeLoaded: true,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -145,7 +157,10 @@ describe('components/Root', () => {
|
|||||||
actions: {
|
actions: {
|
||||||
...baseProps.actions,
|
...baseProps.actions,
|
||||||
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
loadConfigAndMe: jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve({data: true});
|
return Promise.resolve({
|
||||||
|
config: {},
|
||||||
|
isMeLoaded: true,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -172,7 +187,7 @@ describe('components/Root', () => {
|
|||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
} as unknown as RouteComponentProps['history'],
|
} as unknown as RouteComponentProps['history'],
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<Root {...props}/>);
|
const wrapper = shallow<Root>(<Root {...props}/>);
|
||||||
expect(props.history.push).not.toHaveBeenCalled();
|
expect(props.history.push).not.toHaveBeenCalled();
|
||||||
const props2 = {
|
const props2 = {
|
||||||
noAccounts: true,
|
noAccounts: true,
|
||||||
@ -188,7 +203,7 @@ describe('components/Root', () => {
|
|||||||
writable: true,
|
writable: true,
|
||||||
});
|
});
|
||||||
window.location.reload = jest.fn();
|
window.location.reload = jest.fn();
|
||||||
const wrapper = shallow(<Root {...baseProps}/>);
|
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()),
|
||||||
@ -207,14 +222,11 @@ describe('components/Root', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not set a TelemetryHandler when onConfigLoaded is called if Rudder is not configured', () => {
|
test('should not set a TelemetryHandler when onConfigLoaded is called if Rudder is not configured', () => {
|
||||||
store.dispatch({
|
const wrapper = shallow<Root>(<Root {...baseProps}/>);
|
||||||
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
|
||||||
data: {
|
|
||||||
ServiceEnvironment: ServiceEnvironment.DEV,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapper = shallow(<Root {...baseProps}/>);
|
wrapper.instance().onConfigLoaded({
|
||||||
|
ServiceEnvironment: ServiceEnvironment.DEV,
|
||||||
|
});
|
||||||
|
|
||||||
Client4.trackEvent('category', 'event');
|
Client4.trackEvent('category', 'event');
|
||||||
|
|
||||||
@ -224,17 +236,12 @@ describe('components/Root', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should set a TelemetryHandler when onConfigLoaded is called if Rudder is configured', () => {
|
test('should set a TelemetryHandler when onConfigLoaded is called if Rudder is configured', () => {
|
||||||
store.dispatch({
|
const wrapper = shallow<Root>(<Root {...baseProps}/>);
|
||||||
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
|
||||||
data: {
|
wrapper.instance().onConfigLoaded({
|
||||||
ServiceEnvironment: ServiceEnvironment.TEST,
|
ServiceEnvironment: ServiceEnvironment.TEST,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapper = shallow(<Root {...baseProps}/>);
|
|
||||||
|
|
||||||
(wrapper.instance() as any).onConfigLoaded();
|
|
||||||
|
|
||||||
Client4.trackEvent('category', 'event');
|
Client4.trackEvent('category', 'event');
|
||||||
|
|
||||||
expect(Client4.telemetryHandler).toBeDefined();
|
expect(Client4.telemetryHandler).toBeDefined();
|
||||||
@ -247,17 +254,12 @@ describe('components/Root', () => {
|
|||||||
// Simulate an error occurring and the callback not getting called
|
// Simulate an error occurring and the callback not getting called
|
||||||
});
|
});
|
||||||
|
|
||||||
store.dispatch({
|
const wrapper = shallow<Root>(<Root {...baseProps}/>);
|
||||||
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
|
||||||
data: {
|
wrapper.instance().onConfigLoaded({
|
||||||
ServiceEnvironment: ServiceEnvironment.TEST,
|
ServiceEnvironment: ServiceEnvironment.PRODUCTION,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapper = shallow(<Root {...baseProps}/>);
|
|
||||||
|
|
||||||
(wrapper.instance() as any).onConfigLoaded();
|
|
||||||
|
|
||||||
Client4.trackEvent('category', 'event');
|
Client4.trackEvent('category', 'event');
|
||||||
|
|
||||||
expect(Client4.telemetryHandler).not.toBeDefined();
|
expect(Client4.telemetryHandler).not.toBeDefined();
|
||||||
@ -287,9 +289,9 @@ describe('components/Root', () => {
|
|||||||
} as unknown as ProductComponent],
|
} as unknown as ProductComponent],
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(<Root {...props}/>);
|
const wrapper = shallow<Root>(<Root {...props}/>);
|
||||||
|
|
||||||
(wrapper.instance() as any).setState({configLoaded: true});
|
wrapper.instance().setState({configLoaded: true});
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
@ -313,8 +315,8 @@ describe('components/Root', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
test('should show for normal cases', () => {
|
test('should show for normal cases', () => {
|
||||||
const wrapper = shallow(<Root {...landingProps}/>);
|
const wrapper = shallow<Root>(<Root {...landingProps}/>);
|
||||||
(wrapper.instance() as any).onConfigLoaded();
|
wrapper.instance().onConfigLoaded({});
|
||||||
expect(landingProps.history.push).toHaveBeenCalledWith('/landing#/');
|
expect(landingProps.history.push).toHaveBeenCalledWith('/landing#/');
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
@ -328,8 +330,8 @@ describe('components/Root', () => {
|
|||||||
},
|
},
|
||||||
} as RouteComponentProps,
|
} as RouteComponentProps,
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<Root {...props}/>);
|
const wrapper = shallow<Root>(<Root {...props}/>);
|
||||||
(wrapper.instance() as any).onConfigLoaded();
|
wrapper.instance().onConfigLoaded({});
|
||||||
expect(props.history.push).not.toHaveBeenCalled();
|
expect(props.history.push).not.toHaveBeenCalled();
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
@ -3,30 +3,23 @@
|
|||||||
|
|
||||||
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 type {UserProfile} from '@mattermost/types/users';
|
|
||||||
|
|
||||||
import {setSystemEmojis} from 'mattermost-redux/actions/emojis';
|
import {setSystemEmojis} from 'mattermost-redux/actions/emojis';
|
||||||
import {setUrl} from 'mattermost-redux/actions/general';
|
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 {General} from 'mattermost-redux/constants';
|
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
|
||||||
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
|
||||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams';
|
|
||||||
import {getCurrentUser, isCurrentUserSystemAdmin, checkIsFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
|
||||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import {loadRecentlyUsedCustomEmojis} from 'actions/emoji_actions';
|
|
||||||
import * as GlobalActions from 'actions/global_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';
|
||||||
import store from 'stores/redux_store';
|
|
||||||
|
|
||||||
import AccessProblem from 'components/access_problem';
|
import AccessProblem from 'components/access_problem';
|
||||||
import AnnouncementBarController from 'components/announcement_bar';
|
import AnnouncementBarController from 'components/announcement_bar';
|
||||||
@ -53,7 +46,7 @@ import webSocketClient from 'client/web_websocket_client';
|
|||||||
import {initializePlugins} from 'plugins';
|
import {initializePlugins} from 'plugins';
|
||||||
import Pluggable from 'plugins/pluggable';
|
import Pluggable from 'plugins/pluggable';
|
||||||
import A11yController from 'utils/a11y_controller';
|
import A11yController from 'utils/a11y_controller';
|
||||||
import {PageLoadContext, StoragePrefixes} from 'utils/constants';
|
import {PageLoadContext} from 'utils/constants';
|
||||||
import {EmojiIndicesByAlias} from 'utils/emoji';
|
import {EmojiIndicesByAlias} from 'utils/emoji';
|
||||||
import {TEAM_NAME_PATH_PATTERN} from 'utils/path';
|
import {TEAM_NAME_PATH_PATTERN} from 'utils/path';
|
||||||
import {getSiteURL} from 'utils/url';
|
import {getSiteURL} from 'utils/url';
|
||||||
@ -62,7 +55,7 @@ import * as Utils from 'utils/utils';
|
|||||||
|
|
||||||
import type {ProductComponent, PluginComponent} from 'types/store/plugins';
|
import type {ProductComponent, PluginComponent} from 'types/store/plugins';
|
||||||
|
|
||||||
import {applyLuxonDefaults} from './effects';
|
import LuxonController from './luxon_controller';
|
||||||
import RootProvider from './root_provider';
|
import RootProvider from './root_provider';
|
||||||
import RootRedirect from './root_redirect';
|
import RootRedirect from './root_redirect';
|
||||||
|
|
||||||
@ -134,12 +127,14 @@ function LoggedInRoute(props: LoggedInRouteProps) {
|
|||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
export type Actions = {
|
export type Actions = {
|
||||||
getFirstAdminSetupComplete: () => Promise<ActionResult>;
|
|
||||||
getProfiles: (page?: number, pageSize?: number, options?: Record<string, any>) => Promise<ActionResult>;
|
getProfiles: (page?: number, pageSize?: number, options?: Record<string, any>) => Promise<ActionResult>;
|
||||||
|
loadRecentlyUsedCustomEmojis: () => Promise<unknown>;
|
||||||
migrateRecentEmojis: () => void;
|
migrateRecentEmojis: () => void;
|
||||||
loadConfigAndMe: () => Promise<ActionResult>;
|
loadConfigAndMe: () => Promise<{config?: Partial<ClientConfig>; isMeLoaded: boolean}>;
|
||||||
registerCustomPostRenderer: (type: string, component: any, id: string) => Promise<ActionResult>;
|
registerCustomPostRenderer: (type: string, component: any, id: string) => Promise<ActionResult>;
|
||||||
initializeProducts: () => Promise<unknown>;
|
initializeProducts: () => Promise<unknown>;
|
||||||
|
handleLoginLogoutSignal: (e: StorageEvent) => unknown;
|
||||||
|
redirectToOnboardingOrDefaultTeam: (history: History) => unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -208,12 +203,9 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.a11yController = new A11yController();
|
this.a11yController = new A11yController();
|
||||||
|
|
||||||
store.subscribe(() => applyLuxonDefaults(store.getState()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfigLoaded = () => {
|
onConfigLoaded = (config: Partial<ClientConfig>) => {
|
||||||
const config = getConfig(store.getState());
|
|
||||||
const telemetryId = this.props.telemetryId;
|
const telemetryId = this.props.telemetryId;
|
||||||
|
|
||||||
const rudderUrl = 'https://pdat.matterlytics.com';
|
const rudderUrl = 'https://pdat.matterlytics.com';
|
||||||
@ -231,7 +223,7 @@ 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 = getConfig(store.getState()).SiteURL;
|
const siteURL = config.SiteURL;
|
||||||
if (siteURL !== '') {
|
if (siteURL !== '') {
|
||||||
try {
|
try {
|
||||||
rudderCfg.setCookieDomain = new URL(siteURL || '').hostname;
|
rudderCfg.setCookieDomain = new URL(siteURL || '').hostname;
|
||||||
@ -293,7 +285,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.props.actions.migrateRecentEmojis();
|
this.props.actions.migrateRecentEmojis();
|
||||||
store.dispatch(loadRecentlyUsedCustomEmojis());
|
this.props.actions.loadRecentlyUsedCustomEmojis();
|
||||||
|
|
||||||
this.showLandingPageIfNecessary();
|
this.showLandingPageIfNecessary();
|
||||||
|
|
||||||
@ -376,50 +368,6 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async redirectToOnboardingOrDefaultTeam() {
|
|
||||||
const storeState = store.getState();
|
|
||||||
const isUserAdmin = isCurrentUserSystemAdmin(storeState);
|
|
||||||
if (!isUserAdmin) {
|
|
||||||
GlobalActions.redirectUserToDefaultTeam();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const teams = getActiveTeamsList(storeState);
|
|
||||||
|
|
||||||
const onboardingFlowEnabled = getIsOnboardingFlowEnabled(storeState);
|
|
||||||
|
|
||||||
if (teams.length > 0 || !onboardingFlowEnabled) {
|
|
||||||
GlobalActions.redirectUserToDefaultTeam();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstAdminSetupComplete = await this.props.actions.getFirstAdminSetupComplete();
|
|
||||||
if (firstAdminSetupComplete?.data) {
|
|
||||||
GlobalActions.redirectUserToDefaultTeam();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profilesResult = await this.props.actions.getProfiles(0, General.PROFILE_CHUNK_SIZE, {roles: General.SYSTEM_ADMIN_ROLE});
|
|
||||||
if (profilesResult.error) {
|
|
||||||
GlobalActions.redirectUserToDefaultTeam();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentUser = getCurrentUser(store.getState());
|
|
||||||
const adminProfiles = profilesResult.data.reduce(
|
|
||||||
(acc: Record<string, UserProfile>, curr: UserProfile) => {
|
|
||||||
acc[curr.id] = curr;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
if (checkIsFirstAdmin(currentUser, adminProfiles)) {
|
|
||||||
this.props.history.push('/preparing-workspace');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GlobalActions.redirectUserToDefaultTeam();
|
|
||||||
}
|
|
||||||
|
|
||||||
captureUTMParams() {
|
captureUTMParams() {
|
||||||
const qs = new URLSearchParams(window.location.search);
|
const qs = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
@ -445,13 +393,15 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initiateMeRequests = async () => {
|
initiateMeRequests = async () => {
|
||||||
const {data: isMeLoaded} = await this.props.actions.loadConfigAndMe();
|
const {config, isMeLoaded} = await this.props.actions.loadConfigAndMe();
|
||||||
|
|
||||||
if (isMeLoaded && this.props.location.pathname === '/') {
|
if (isMeLoaded && this.props.location.pathname === '/') {
|
||||||
this.redirectToOnboardingOrDefaultTeam();
|
this.props.actions.redirectToOnboardingOrDefaultTeam(this.props.history);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onConfigLoaded();
|
if (config) {
|
||||||
|
this.onConfigLoaded(config);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -475,28 +425,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleLogoutLoginSignal = (e: StorageEvent) => {
|
handleLogoutLoginSignal = (e: StorageEvent) => {
|
||||||
// when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
|
this.props.actions.handleLoginLogoutSignal(e);
|
||||||
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(store.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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setRootMeta = () => {
|
setRootMeta = () => {
|
||||||
@ -519,6 +448,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<RootProvider>
|
<RootProvider>
|
||||||
<MobileViewWatcher/>
|
<MobileViewWatcher/>
|
||||||
|
<LuxonController/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
path={'/error'}
|
path={'/error'}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {LogLevel} from '@mattermost/types/client4';
|
import {LogLevel} from '@mattermost/types/client4';
|
||||||
|
import type {ClientConfig} from '@mattermost/types/config';
|
||||||
import type {SystemSetting} from '@mattermost/types/general';
|
import type {SystemSetting} from '@mattermost/types/general';
|
||||||
|
|
||||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||||
@ -12,7 +13,7 @@ import {logError} from './errors';
|
|||||||
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
|
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
|
||||||
import {loadRolesIfNeeded} from './roles';
|
import {loadRolesIfNeeded} from './roles';
|
||||||
|
|
||||||
export function getClientConfig(): ActionFuncAsync {
|
export function getClientConfig(): ActionFuncAsync<ClientConfig> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
|
Loading…
Reference in New Issue
Block a user