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:
Harrison Healey 2024-04-22 13:31:27 -04:00 committed by GitHub
parent 39ba2e72c0
commit f3b80f77a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 197 additions and 164 deletions

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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 {